#include "Merc2.h" #include "game/graphics/opengl_renderer/background/background_common.h" #include "third-party/imgui/imgui.h" Merc2::Merc2(const std::string& name, int my_id) : BucketRenderer(name, my_id) { glGenVertexArrays(1, &m_vao); glBindVertexArray(m_vao); glGenBuffers(1, &m_bones_buffer); glBindBuffer(GL_UNIFORM_BUFFER, m_bones_buffer); std::vector temp(MAX_SHADER_BONE_VECTORS * sizeof(math::Vector4f)); glBufferData(GL_UNIFORM_BUFFER, MAX_SHADER_BONE_VECTORS * sizeof(math::Vector4f), temp.data(), GL_DYNAMIC_DRAW); glBindBuffer(GL_UNIFORM_BUFFER, 0); GLint val; glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &val); if (val <= 16) { // somehow doubt this can happen, but just in case m_opengl_buffer_alignment = 1; } else { m_opengl_buffer_alignment = val / 16; // number of bone vectors if (m_opengl_buffer_alignment * 16 != (u32)val) { ASSERT_MSG(false, fmt::format("opengl uniform buffer alignment is {}, which is strange\n", val)); } } for (int i = 0; i < MAX_LEVELS; i++) { auto& draws = m_level_draw_buckets.emplace_back(); draws.draws.resize(MAX_DRAWS_PER_LEVEL); draws.envmap_draws.resize(MAX_ENVMAP_DRAWS_PER_LEVEL); } } /*! * Handle the merc renderer switching to a different model. */ void Merc2::init_pc_model(const DmaTransfer& setup, SharedRenderState* render_state) { // ;; name (128 char, 8 qw) // ;; lights (7 qw x 1) // ;; matrix slot string (128 char, 8 qw) // ;; matrices (7 qw x N) // ;; flags (num-effects, effect-alpha-ignore, effect-disable) // ;; fades (u32 x N), padding to qw aligned // Part 1: name const u8* input_data = setup.data; char name[128]; strcpy(name, (const char*)setup.data); m_current_model = render_state->loader->get_merc_model(name); input_data += 128; // Part 2: lights memcpy(&m_current_lights, input_data, sizeof(VuLights)); input_data += sizeof(VuLights); // Part 3: matrix slot string auto* matrix_array = (const u32*)(input_data + 128); int i; for (i = 0; i < 128; i++) { if (input_data[i] == 0xff) { break; } u32 addr; memcpy(&addr, &matrix_array[i * 4], 4); const u8* real_addr = setup.data - setup.data_offset + addr; memcpy(&m_skel_matrix_buffer[input_data[i]], real_addr, sizeof(MercMat)); } input_data += 128 + 16 * i; // Part 4: flags auto* flags = (const u32*)input_data; int num_effects = flags[0]; m_current_ignore_alpha_bits = flags[1]; m_current_effect_enable_bits = flags[2]; input_data += 16; // Part 5: fades for (int ei = 0; ei < num_effects; ei++) { for (int j = 0; j < 4; j++) { m_fade_buffer[ei * 4 + j] = input_data[ei * 4 + j]; } } if (m_current_model) { m_stats.num_models++; for (const auto& effect : m_current_model->model->effects) { bool envmap = effect.has_envmap; m_stats.num_effects++; m_stats.num_predicted_draws += effect.draws.size(); if (envmap) { m_stats.num_envmap_effects++; m_stats.num_predicted_draws += effect.draws.size(); } for (const auto& draw : effect.draws) { m_stats.num_predicted_tris += draw.num_triangles; if (envmap) { m_stats.num_predicted_tris += draw.num_triangles; } } } } else { m_stats.num_missing_models++; } } void Merc2::draw_debug_window() { ImGui::Text("Models : %d", m_stats.num_models); ImGui::Text("Effects : %d", m_stats.num_effects); ImGui::Text("Draws (p): %d", m_stats.num_predicted_draws); ImGui::Text("Tris (p): %d", m_stats.num_predicted_tris); ImGui::Text("Bones : %d", m_stats.num_bones_uploaded); ImGui::Text("Lights : %d", m_stats.num_lights); ImGui::Text("Dflush : %d", m_stats.num_draw_flush); ImGui::Text("EEffects : %d", m_stats.num_envmap_effects); ImGui::Text("ETris : %d", m_stats.num_envmap_tris); } void Merc2::init_shaders(ShaderLibrary& shaders) { init_shader_common(shaders[ShaderId::MERC2], &m_merc_uniforms, true); init_shader_common(shaders[ShaderId::EMERC], &m_emerc_uniforms, false); m_emerc_uniforms.fade = glGetUniformLocation(shaders[ShaderId::EMERC].id(), "fade"); } void Merc2::init_shader_common(Shader& shader, Uniforms* uniforms, bool include_lights) { auto id = shader.id(); shader.activate(); if (include_lights) { uniforms->light_direction[0] = glGetUniformLocation(id, "light_dir0"); uniforms->light_direction[1] = glGetUniformLocation(id, "light_dir1"); uniforms->light_direction[2] = glGetUniformLocation(id, "light_dir2"); uniforms->light_color[0] = glGetUniformLocation(id, "light_col0"); uniforms->light_color[1] = glGetUniformLocation(id, "light_col1"); uniforms->light_color[2] = glGetUniformLocation(id, "light_col2"); uniforms->light_ambient = glGetUniformLocation(id, "light_ambient"); } uniforms->hvdf_offset = glGetUniformLocation(id, "hvdf_offset"); uniforms->fog = glGetUniformLocation(id, "fog_constants"); uniforms->decal = glGetUniformLocation(id, "decal_enable"); uniforms->fog_color = glGetUniformLocation(id, "fog_color"); uniforms->perspective_matrix = glGetUniformLocation(id, "perspective_matrix"); uniforms->ignore_alpha = glGetUniformLocation(id, "ignore_alpha"); uniforms->gfx_hack_no_tex = glGetUniformLocation(id, "gfx_hack_no_tex"); } void Merc2::switch_to_merc2(SharedRenderState* render_state) { render_state->shaders[ShaderId::MERC2].activate(); // set uniforms that we know from render_state glUniform4f(m_merc_uniforms.fog_color, render_state->fog_color[0] / 255.f, render_state->fog_color[1] / 255.f, render_state->fog_color[2] / 255.f, render_state->fog_intensity / 255); glUniform1i(m_merc_uniforms.gfx_hack_no_tex, Gfx::g_global_settings.hack_no_tex); } void Merc2::switch_to_emerc(SharedRenderState* render_state) { render_state->shaders[ShaderId::EMERC].activate(); // set uniforms that we know from render_state glUniform4f(m_emerc_uniforms.fog_color, render_state->fog_color[0] / 255.f, render_state->fog_color[1] / 255.f, render_state->fog_color[2] / 255.f, render_state->fog_intensity / 255); glUniform1i(m_emerc_uniforms.gfx_hack_no_tex, Gfx::g_global_settings.hack_no_tex); } /*! * Main merc2 rendering. */ void Merc2::render(DmaFollower& dma, SharedRenderState* render_state, ScopedProfilerNode& prof) { m_stats = {}; // skip if disabled if (!m_enabled) { while (dma.current_tag_offset() != render_state->next_bucket) { dma.read_and_advance(); } return; } m_current_model = std::nullopt; switch_to_merc2(render_state); // iterate through the dma chain, filling buckets handle_all_dma(dma, render_state, prof); // flush model data to buckets flush_pending_model(render_state, prof); // flush buckets to draws flush_draw_buckets(render_state, prof); } u32 Merc2::alloc_lights(const VuLights& lights) { ASSERT(m_next_free_light < MAX_LIGHTS); m_stats.num_lights++; u32 light_idx = m_next_free_light; m_lights_buffer[m_next_free_light++] = lights; static_assert(sizeof(VuLights) == 7 * 16); return light_idx; } std::string Merc2::ShaderMercMat::to_string() const { return fmt::format("tmat:\n{}\n{}\n{}\n{}\n", tmat[0].to_string_aligned(), tmat[1].to_string_aligned(), tmat[2].to_string_aligned(), tmat[3].to_string_aligned()); } /*! * Main MERC2 function to handle DMA */ void Merc2::handle_all_dma(DmaFollower& dma, SharedRenderState* render_state, ScopedProfilerNode& prof) { // process the first tag. this is just jumping to the merc-specific dma. auto data0 = dma.read_and_advance(); ASSERT(data0.vif1() == 0 || data0.vifcode1().kind == VifCode::Kind::NOP); ASSERT(data0.vif0() == 0 || data0.vifcode0().kind == VifCode::Kind::NOP || data0.vifcode0().kind == VifCode::Kind::MARK); ASSERT(data0.size_bytes == 0); if (dma.current_tag().kind == DmaTag::Kind::CALL) { // renderer didn't run, let's just get out of here. for (int i = 0; i < 4; i++) { dma.read_and_advance(); } ASSERT(dma.current_tag_offset() == render_state->next_bucket); return; } if (dma.current_tag_offset() == render_state->next_bucket) { return; } // if we reach here, there's stuff to draw // this handles merc-specific setup DMA handle_setup_dma(dma, render_state); // handle each merc transfer while (dma.current_tag_offset() != render_state->next_bucket) { handle_merc_chain(dma, render_state, prof); } ASSERT(dma.current_tag_offset() == render_state->next_bucket); } namespace { void set_uniform(GLuint uniform, const math::Vector3f& val) { glUniform3f(uniform, val.x(), val.y(), val.z()); } void set_uniform(GLuint uniform, const math::Vector4f& val) { glUniform4f(uniform, val.x(), val.y(), val.z(), val.w()); } } // namespace void Merc2::handle_setup_dma(DmaFollower& dma, SharedRenderState* render_state) { auto first = dma.read_and_advance(); // 10 quadword setup packet ASSERT(first.size_bytes == 10 * 16); // transferred vifcodes { auto vif0 = first.vifcode0(); auto vif1 = first.vifcode1(); // STCYCL 4, 4 ASSERT(vif0.kind == VifCode::Kind::STCYCL); auto vif0_st = VifCodeStcycl(vif0); ASSERT(vif0_st.cl == 4 && vif0_st.wl == 4); // STMOD ASSERT(vif1.kind == VifCode::Kind::STMOD); ASSERT(vif1.immediate == 0); } // 1 qw with 4 vifcodes. u32 vifcode_data[4]; memcpy(vifcode_data, first.data, 16); { auto vif0 = VifCode(vifcode_data[0]); ASSERT(vif0.kind == VifCode::Kind::BASE); ASSERT(vif0.immediate == MercDataMemory::BUFFER_BASE); auto vif1 = VifCode(vifcode_data[1]); ASSERT(vif1.kind == VifCode::Kind::OFFSET); ASSERT((s16)vif1.immediate == MercDataMemory::BUFFER_OFFSET); auto vif2 = VifCode(vifcode_data[2]); ASSERT(vif2.kind == VifCode::Kind::NOP); auto vif3 = VifCode(vifcode_data[3]); ASSERT(vif3.kind == VifCode::Kind::UNPACK_V4_32); VifCodeUnpack up(vif3); ASSERT(up.addr_qw == MercDataMemory::LOW_MEMORY); ASSERT(!up.use_tops_flag); ASSERT(vif3.num == 8); } // 8 qw's of low memory data memcpy(&m_low_memory, first.data + 16, sizeof(LowMemory)); switch_to_merc2(render_state); set_uniform(m_merc_uniforms.hvdf_offset, m_low_memory.hvdf_offset); set_uniform(m_merc_uniforms.fog, m_low_memory.fog); glUniformMatrix4fv(m_merc_uniforms.perspective_matrix, 1, GL_FALSE, &m_low_memory.perspective[0].x()); switch_to_emerc(render_state); set_uniform(m_emerc_uniforms.hvdf_offset, m_low_memory.hvdf_offset); set_uniform(m_emerc_uniforms.fog, m_low_memory.fog); glUniformMatrix4fv(m_emerc_uniforms.perspective_matrix, 1, GL_FALSE, &m_low_memory.perspective[0].x()); // 1 qw with another 4 vifcodes. u32 vifcode_final_data[4]; memcpy(vifcode_final_data, first.data + 16 + sizeof(LowMemory), 16); { ASSERT(VifCode(vifcode_final_data[0]).kind == VifCode::Kind::FLUSHE); ASSERT(vifcode_final_data[1] == 0); ASSERT(vifcode_final_data[2] == 0); VifCode mscal(vifcode_final_data[3]); ASSERT(mscal.kind == VifCode::Kind::MSCAL); ASSERT(mscal.immediate == 0); } // TODO: process low memory initialization if (render_state->version == GameVersion::Jak1) { auto second = dma.read_and_advance(); ASSERT(second.size_bytes == 32); // setting up test register. auto nothing = dma.read_and_advance(); ASSERT(nothing.size_bytes == 0); ASSERT(nothing.vif0() == 0); ASSERT(nothing.vif1() == 0); } else { auto second = dma.read_and_advance(); ASSERT(second.size_bytes == 48); // setting up test/zbuf register. // todo z write mask stuff. auto nothing = dma.read_and_advance(); ASSERT(nothing.size_bytes == 0); ASSERT(nothing.vif0() == 0); ASSERT(nothing.vif1() == 0); } } namespace { bool tag_is_nothing_next(const DmaFollower& dma) { return dma.current_tag().kind == DmaTag::Kind::NEXT && dma.current_tag().qwc == 0 && dma.current_tag_vif0() == 0 && dma.current_tag_vif1() == 0; } } // namespace void Merc2::handle_merc_chain(DmaFollower& dma, SharedRenderState* render_state, ScopedProfilerNode& prof) { while (tag_is_nothing_next(dma)) { auto nothing = dma.read_and_advance(); ASSERT(nothing.size_bytes == 0); } if (dma.current_tag().kind == DmaTag::Kind::CALL) { for (int i = 0; i < 4; i++) { dma.read_and_advance(); } return; } auto init = dma.read_and_advance(); int skip_count = 2; if (render_state->version == GameVersion::Jak2) { skip_count = 1; } while (init.vifcode1().kind == VifCode::Kind::PC_PORT) { flush_pending_model(render_state, prof); init_pc_model(init, render_state); for (int i = 0; i < skip_count; i++) { auto link = dma.read_and_advance(); ASSERT(link.vifcode0().kind == VifCode::Kind::NOP); ASSERT(link.vifcode1().kind == VifCode::Kind::NOP); ASSERT(link.size_bytes == 0); } init = dma.read_and_advance(); } if (init.vifcode0().kind == VifCode::Kind::FLUSHA) { int num_skipped = 0; while (dma.current_tag_offset() != render_state->next_bucket) { dma.read_and_advance(); num_skipped++; } ASSERT(num_skipped < 4); return; } } /*! * Queue up some bones to be included in the bone buffer. * Returns the index of the first bone vector. */ u32 Merc2::alloc_bones(int count) { u32 first_bone_vector = m_next_free_bone_vector; ASSERT(count * 8 + first_bone_vector <= MAX_SHADER_BONE_VECTORS); // model should have under 128 bones. ASSERT(count <= MAX_SKEL_BONES); // iterate over each bone we need for (int i = 0; i < count; i++) { auto& skel_mat = m_skel_matrix_buffer[i]; auto* shader_mat = &m_shader_bone_vector_buffer[m_next_free_bone_vector]; int bv = 0; // and copy to the large bone buffer. for (int j = 0; j < 4; j++) { shader_mat[bv++] = skel_mat.tmat[j]; } for (int j = 0; j < 3; j++) { shader_mat[bv++] = skel_mat.nmat[j]; } m_next_free_bone_vector += 8; } auto b0 = m_next_free_bone_vector; m_next_free_bone_vector += m_opengl_buffer_alignment - 1; m_next_free_bone_vector /= m_opengl_buffer_alignment; m_next_free_bone_vector *= m_opengl_buffer_alignment; ASSERT(b0 <= m_next_free_bone_vector); ASSERT(first_bone_vector + count * 8 <= m_next_free_bone_vector); return first_bone_vector; } /*! * Flush a model to draw buckets */ void Merc2::flush_pending_model(SharedRenderState* render_state, ScopedProfilerNode& prof) { if (!m_current_model) { return; } const LevelData* lev = m_current_model->level; const tfrag3::MercModel* model = m_current_model->model; int bone_count = model->max_bones + 1; if (m_next_free_light >= MAX_LIGHTS) { fmt::print("MERC2 out of lights, consider increasing MAX_LIGHTS\n"); flush_draw_buckets(render_state, prof); } if (m_next_free_bone_vector + m_opengl_buffer_alignment + bone_count * 8 > MAX_SHADER_BONE_VECTORS) { fmt::print("MERC2 out of bones, consider increasing MAX_SHADER_BONE_VECTORS\n"); flush_draw_buckets(render_state, prof); } // find a level bucket LevelDrawBucket* lev_bucket = nullptr; for (u32 i = 0; i < m_next_free_level_bucket; i++) { if (m_level_draw_buckets[i].level == lev) { lev_bucket = &m_level_draw_buckets[i]; break; } } if (!lev_bucket) { // no existing bucket if (m_next_free_level_bucket >= m_level_draw_buckets.size()) { // out of room, flush // fmt::print("MERC2 out of levels, consider increasing MAX_LEVELS\n"); flush_draw_buckets(render_state, prof); // and retry the whole thing. flush_pending_model(render_state, prof); return; } // alloc a new one lev_bucket = &m_level_draw_buckets[m_next_free_level_bucket++]; lev_bucket->reset(); lev_bucket->level = lev; } if (lev_bucket->next_free_draw + model->max_draws >= lev_bucket->draws.size()) { // out of room, flush fmt::print("MERC2 out of draws, consider increasing MAX_DRAWS_PER_LEVEL\n"); flush_draw_buckets(render_state, prof); // and retry the whole thing. flush_pending_model(render_state, prof); return; } if (lev_bucket->next_free_envmap_draw + model->max_draws >= lev_bucket->envmap_draws.size()) { // out of room, flush fmt::print("MERC2 out of envmap draws, consider increasing MAX_ENVMAP_DRAWS_PER_LEVEL\n"); // or, use a more accurate max_draws for envmap. flush_draw_buckets(render_state, prof); // and retry the whole thing. flush_pending_model(render_state, prof); return; } u32 first_bone = alloc_bones(bone_count); // allocate lights u32 lights = alloc_lights(m_current_lights); // for (size_t ei = 0; ei < model->effects.size(); ei++) { if (!(m_current_effect_enable_bits & (1 << ei))) { continue; } u8 ignore_alpha = (m_current_ignore_alpha_bits & (1 << ei)); auto& effect = model->effects[ei]; if (effect.has_envmap) { bool nonzero_fade = false; for (int i = 0; i < 4; i++) { if (m_fade_buffer[4 * ei + i]) { nonzero_fade = true; break; } } if (nonzero_fade) { for (auto& mdraw : effect.draws) { Draw* draw = &lev_bucket->envmap_draws[lev_bucket->next_free_envmap_draw++]; draw->first_index = mdraw.first_index; draw->index_count = mdraw.index_count; draw->mode = effect.envmap_mode; draw->texture = effect.envmap_texture; draw->first_bone = first_bone; draw->light_idx = lights; draw->num_triangles = mdraw.num_triangles; draw->ignore_alpha = false; for (int i = 0; i < 4; i++) { draw->fade[i] = m_fade_buffer[4 * ei + i]; } } } } for (auto& mdraw : effect.draws) { Draw* draw = &lev_bucket->draws[lev_bucket->next_free_draw++]; draw->first_index = mdraw.first_index; draw->index_count = mdraw.index_count; draw->mode = mdraw.mode; draw->texture = mdraw.tree_tex_id; draw->first_bone = first_bone; draw->light_idx = lights; draw->num_triangles = mdraw.num_triangles; draw->ignore_alpha = ignore_alpha; for (int i = 0; i < 4; i++) { draw->fade[i] = 0; } } } m_current_model = std::nullopt; } void Merc2::flush_draw_buckets(SharedRenderState* render_state, ScopedProfilerNode& prof) { m_stats.num_draw_flush++; for (u32 li = 0; li < m_next_free_level_bucket; li++) { const auto& lev_bucket = m_level_draw_buckets[li]; const auto* lev = lev_bucket.level; glBindVertexArray(m_vao); glBindBuffer(GL_ARRAY_BUFFER, lev->merc_vertices); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, lev->merc_indices); glEnable(GL_PRIMITIVE_RESTART); glPrimitiveRestartIndex(UINT32_MAX); glEnableVertexAttribArray(0); glEnableVertexAttribArray(1); glEnableVertexAttribArray(2); glEnableVertexAttribArray(3); glEnableVertexAttribArray(4); glEnableVertexAttribArray(5); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_GEQUAL); glVertexAttribPointer(0, // location 0 in the shader 3, // 3 values per vert GL_FLOAT, // floats GL_FALSE, // normalized sizeof(tfrag3::MercVertex), // stride (void*)offsetof(tfrag3::MercVertex, pos) // offset (0) ); glVertexAttribPointer(1, // location 1 in the 3, // 3 values per vert GL_FLOAT, // floats GL_FALSE, // normalized sizeof(tfrag3::MercVertex), // stride (void*)offsetof(tfrag3::MercVertex, normal[0]) // offset (0) ); glVertexAttribPointer(2, // location 1 in the 3, // 3 values per vert GL_FLOAT, // floats GL_FALSE, // normalized sizeof(tfrag3::MercVertex), // stride (void*)offsetof(tfrag3::MercVertex, weights[0]) // offset (0) ); glVertexAttribPointer(3, // location 1 in the shader 2, // 3 values per vert GL_FLOAT, // floats GL_FALSE, // normalized sizeof(tfrag3::MercVertex), // stride (void*)offsetof(tfrag3::MercVertex, st[0]) // offset (0) ); glVertexAttribPointer(4, // location 1 in the shader 4, // 3 values per vert GL_UNSIGNED_BYTE, // floats GL_TRUE, // normalized sizeof(tfrag3::MercVertex), // stride (void*)offsetof(tfrag3::MercVertex, rgba[0]) // offset (0) ); glVertexAttribIPointer(5, // location 0 in the 4, // 3 floats per vert GL_UNSIGNED_BYTE, // u8's sizeof(tfrag3::MercVertex), // (void*)offsetof(tfrag3::MercVertex, mats[0]) // offset in array ); m_stats.num_bones_uploaded += m_next_free_bone_vector; glBindBuffer(GL_UNIFORM_BUFFER, m_bones_buffer); glBufferSubData(GL_UNIFORM_BUFFER, 0, m_next_free_bone_vector * sizeof(math::Vector4f), m_shader_bone_vector_buffer); glBindBuffer(GL_UNIFORM_BUFFER, 0); switch_to_merc2(render_state); do_draws(lev_bucket.draws.data(), lev, lev_bucket.next_free_draw, m_merc_uniforms, prof, false, render_state); if (lev_bucket.next_free_envmap_draw) { switch_to_emerc(render_state); do_draws(lev_bucket.envmap_draws.data(), lev, lev_bucket.next_free_envmap_draw, m_emerc_uniforms, prof, true, render_state); } } m_next_free_light = 0; m_next_free_bone_vector = 0; m_next_free_level_bucket = 0; } void Merc2::do_draws(const Draw* draw_array, const LevelData* lev, u32 num_draws, const Uniforms& uniforms, ScopedProfilerNode& prof, bool set_fade, SharedRenderState* render_state) { int last_tex = -1; int last_light = -1; for (u32 di = 0; di < num_draws; di++) { auto& draw = draw_array[di]; glUniform1i(uniforms.ignore_alpha, draw.ignore_alpha); if ((int)draw.texture != last_tex) { if (draw.texture < lev->textures.size()) { glBindTexture(GL_TEXTURE_2D, lev->textures.at(draw.texture)); } else { fmt::print("Invalid draw.texture is {}, would have crashed.\n", draw.texture); } last_tex = draw.texture; } if ((int)draw.light_idx != last_light && !set_fade) { set_uniform(uniforms.light_direction[0], m_lights_buffer[draw.light_idx].direction0); set_uniform(uniforms.light_direction[1], m_lights_buffer[draw.light_idx].direction1); set_uniform(uniforms.light_direction[2], m_lights_buffer[draw.light_idx].direction2); set_uniform(uniforms.light_color[0], m_lights_buffer[draw.light_idx].color0); set_uniform(uniforms.light_color[1], m_lights_buffer[draw.light_idx].color1); set_uniform(uniforms.light_color[2], m_lights_buffer[draw.light_idx].color2); set_uniform(uniforms.light_ambient, m_lights_buffer[draw.light_idx].ambient); last_light = draw.light_idx; } setup_opengl_from_draw_mode(draw.mode, GL_TEXTURE0, true); glUniform1i(uniforms.decal, draw.mode.get_decal()); if (set_fade) { math::Vector4f fade = math::Vector4f(draw.fade[0], draw.fade[1], draw.fade[2], draw.fade[3]) / 255.f; set_uniform(uniforms.fade, fade); ASSERT(draw.mode.get_alpha_blend() == DrawMode::AlphaBlend::SRC_0_DST_DST); // glBindTexture(GL_TEXTURE_2D, render_state->texture_pool->get_placeholder_texture()); } prof.add_draw_call(); prof.add_tri(draw.num_triangles); glBindBufferRange(GL_UNIFORM_BUFFER, 1, m_bones_buffer, sizeof(math::Vector4f) * draw.first_bone, 128 * sizeof(ShaderMercMat)); glDrawElements(GL_TRIANGLE_STRIP, draw.index_count, GL_UNSIGNED_INT, (void*)(sizeof(u32) * draw.first_index)); } }