text: #include "Merc2.h"
  #include "third-party/imgui/imgui.h"
  #include "game/graphics/opengl_renderer/background/background_common.h"
  
  Merc2::Merc2(const std::string& name, BucketId 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);
    glBufferData(GL_UNIFORM_BUFFER, MAX_SHADER_BONE_VECTORS * sizeof(math::Vector4f), nullptr,
                 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);
    }
  }
  
  /*!
   * Handle the merc renderer switching to a different model.
   */
  void Merc2::init_pc_model(const DmaTransfer& setup, SharedRenderState* render_state) {
    // determine the name. We've packed this in a separate PC-port specific packet.
    char name[128];
    strcpy(name, (const char*)setup.data);
  
    // get the model from the loader
    m_current_model = render_state->loader->get_merc_model(name);
  
    // update stats
    m_stats.num_models++;
    if (m_current_model) {
      for (const auto& effect : m_current_model->model->effects) {
        m_stats.num_effects++;
        m_stats.num_predicted_draws += effect.draws.size();
        for (const auto& draw : effect.draws) {
          m_stats.num_predicted_tris += draw.num_triangles;
        }
      }
    }
  }
  
  /*!
   * Once-per-frame initialization
   */
  void Merc2::init_for_frame(SharedRenderState* render_state) {
    // reset state
    m_current_model = std::nullopt;
    m_stats = {};
  
    // activate the merc shader used for all draws
    render_state->shaders[ShaderId::MERC2].activate();
  
    // set uniforms that we know from render_state
    glUniform4f(m_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);
  }
  
  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);
  }
  
  void Merc2::init_shaders(ShaderLibrary& shaders) {
    shaders[ShaderId::MERC2].activate();
    m_uniforms.light_direction[0] = glGetUniformLocation(shaders[ShaderId::MERC2].id(), "light_dir0");
    m_uniforms.light_direction[1] = glGetUniformLocation(shaders[ShaderId::MERC2].id(), "light_dir1");
    m_uniforms.light_direction[2] = glGetUniformLocation(shaders[ShaderId::MERC2].id(), "light_dir2");
    m_uniforms.light_color[0] = glGetUniformLocation(shaders[ShaderId::MERC2].id(), "light_col0");
    m_uniforms.light_color[1] = glGetUniformLocation(shaders[ShaderId::MERC2].id(), "light_col1");
    m_uniforms.light_color[2] = glGetUniformLocation(shaders[ShaderId::MERC2].id(), "light_col2");
    m_uniforms.light_ambient = glGetUniformLocation(shaders[ShaderId::MERC2].id(), "light_ambient");
  
    m_uniforms.hvdf_offset = glGetUniformLocation(shaders[ShaderId::MERC2].id(), "hvdf_offset");
    m_uniforms.perspective[0] = glGetUniformLocation(shaders[ShaderId::MERC2].id(), "perspective0");
    m_uniforms.perspective[1] = glGetUniformLocation(shaders[ShaderId::MERC2].id(), "perspective1");
    m_uniforms.perspective[2] = glGetUniformLocation(shaders[ShaderId::MERC2].id(), "perspective2");
    m_uniforms.perspective[3] = glGetUniformLocation(shaders[ShaderId::MERC2].id(), "perspective3");
  
    m_uniforms.fog = glGetUniformLocation(shaders[ShaderId::MERC2].id(), "fog_constants");
    m_uniforms.decal = glGetUniformLocation(shaders[ShaderId::MERC2].id(), "decal_enable");
  
    m_uniforms.fog_color = glGetUniformLocation(shaders[ShaderId::MERC2].id(), "fog_color");
    m_uniforms.perspective_matrix =
        glGetUniformLocation(shaders[ShaderId::MERC2].id(), "perspective_matrix");
    m_uniforms.ignore_alpha = glGetUniformLocation(shaders[ShaderId::MERC2].id(), "ignore_alpha");
  }
  
  /*!
   * Main merc2 rendering.
   */
  void Merc2::render(DmaFollower& dma, SharedRenderState* render_state, ScopedProfilerNode& prof) {
    // skip if disabled
    if (!m_enabled) {
      while (dma.current_tag_offset() != render_state->next_bucket) {
        dma.read_and_advance();
      }
      return;
    }
  
    init_for_frame(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;
  }
  
  /*!
   * Store light values
   */
  void Merc2::set_lights(const DmaTransfer& dma) {
    memcpy(&m_current_lights, dma.data, sizeof(VuLights));
  }
  
  void Merc2::handle_matrix_dma(const DmaTransfer& dma) {
    int slot = dma.vif0() & 0xff;
    ASSERT(slot < MAX_SKEL_BONES);
    memcpy(&m_skel_matrix_buffer[slot], dma.data, sizeof(MercMat));
  }
  
  /*!
   * 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);
    ASSERT(data0.vif0() == 0);
    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;
    }
    ASSERT(data0.size_bytes == 0);
    ASSERT(data0.vif0() == 0);
    ASSERT(data0.vif1() == 0);
  
    // if we reach here, there's stuff to draw
    // this handles merc-specific setup DMA
    handle_setup_dma(dma);
  
    // 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) {
    auto first = dma.read_and_advance();
  
    // 10 quadword setup packet
    ASSERT(first.size_bytes == 10 * 16);
    // m_stats.str += fmt::format("Setup 0: {} {} {}", first.size_bytes / 16,
    // first.vifcode0().print(), first.vifcode1().print());
  
    // 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));
    set_uniform(m_uniforms.hvdf_offset, m_low_memory.hvdf_offset);
    set_uniform(m_uniforms.fog, m_low_memory.fog);
    for (int i = 0; i < 4; i++) {
      set_uniform(m_uniforms.perspective[i], m_low_memory.perspective[i]);
    }
    // todo rm.
    glUniformMatrix4fv(m_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
  
    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);
  }
  
  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;
  }
  bool tag_is_nothing_cnt(const DmaFollower& dma) {
    return dma.current_tag().kind == DmaTag::Kind::CNT && 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();
  
    if (init.vifcode1().kind == VifCode::Kind::PC_PORT) {
      // we got a PC PORT packet. this contains some extra data to set up the model
      flush_pending_model(render_state, prof);
      init_pc_model(init, render_state);
      ASSERT(tag_is_nothing_cnt(dma));
      init = dma.read_and_advance();  // dummy tag in pc port
      init = dma.read_and_advance();
    }
  
    // row stuff.
    ASSERT(init.vifcode0().kind == VifCode::Kind::STROW);
    ASSERT(init.size_bytes == 16);
    // m_vif.row[0] = init.vif1();
    // memcpy(m_vif.row + 1, init.data, 12);
    u32 extra;
    memcpy(&extra, init.data + 12, 4);
    // ASSERT(extra == 0);
    m_current_effect_enable_bits = extra;
    m_current_ignore_alpha_bits = extra >> 16;
    DmaTransfer next;
  
    bool setting_up = true;
    u32 mscal_addr = -1;
    while (setting_up) {
      next = dma.read_and_advance();
      // fmt::print("next: {}", dma.current_tag().print());
      u32 offset_in_data = 0;
      //    fmt::print("START {} : {} {}\n", next.size_bytes, next.vifcode0().print(),
      //               next.vifcode1().print());
      auto vif0 = next.vifcode0();
      switch (vif0.kind) {
        case VifCode::Kind::NOP:
        case VifCode::Kind::FLUSHE:
          break;
        case VifCode::Kind::STMOD:
          ASSERT(vif0.immediate == 0 || vif0.immediate == 1);
          // m_vif.stmod = vif0.immediate;
          break;
        default:
          ASSERT(false);
      }
  
      auto vif1 = next.vifcode1();
      switch (vif1.kind) {
        case VifCode::Kind::UNPACK_V4_8: {
          VifCodeUnpack up(vif1);
          offset_in_data += 4 * vif1.num;
        } break;
        case VifCode::Kind::UNPACK_V4_32: {
          VifCodeUnpack up(vif1);
          if (up.addr_qw == 132 && vif1.num == 8) {
            set_lights(next);
          } else if (vif1.num == 7) {
            handle_matrix_dma(next);
          }
          offset_in_data += 16 * vif1.num;
        } break;
        case VifCode::Kind::MSCAL:
          // fmt::print("cal\n");
          mscal_addr = vif1.immediate;
          ASSERT(next.size_bytes == 0);
          setting_up = false;
          break;
        default:
          ASSERT(false);
      }
  
      ASSERT(offset_in_data <= next.size_bytes);
      if (offset_in_data < next.size_bytes) {
        ASSERT((offset_in_data % 4) == 0);
        u32 leftover = next.size_bytes - offset_in_data;
        if (leftover < 16) {
          for (u32 i = 0; i < leftover; i++) {
            ASSERT(next.data[offset_in_data + i] == 0);
          }
        } else {
          ASSERT(false);
        }
      }
    }
  }
  
  /*!
   * 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, float scale) {
    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;
  
      // scale the transformation matrix (todo: can we move this to the extraction)
      // and copy to the large bone buffer.
      for (int j = 0; j < 3; j++) {
        shader_mat[bv++] = skel_mat.tmat[j] * scale;
      }
      shader_mat[bv++] = skel_mat.tmat[3];
  
      for (int j = 0; j < 3; j++) {
        shader_mat[bv++] = skel_mat.nmat[j];
      }
  
      // we could include the effect of the perspective matrix here.
      //        for (int j = 0; j < 3; j++) {
      //          tbone_buffer[i][j] = vf15.elementwise_multiply(bone_mat[j]);
      //          tbone_buffer[i][j].w() += p.w() * bone_mat[j].z();
      //          tbone_buffer[i][j] *= scale;
      //        }
      //
      //        tbone_buffer[i][3] = vf15.elementwise_multiply(bone_mat[3]) +
      //        m_low_memory.perspective[3]; tbone_buffer[i][3].w() += p.w() * bone_mat[3].z();
  
      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;
    }
  
    u32 first_bone = alloc_bones(bone_count, model->scale_xyz);
  
    // 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];
      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;
      }
    }
  
    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
                            3,                                            // 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
                             3,                                            // 3 floats per vert
                             GL_UNSIGNED_BYTE,                             // u8's
                             sizeof(tfrag3::MercVertex),                   //
                             (void*)offsetof(tfrag3::MercVertex, mats[0])  // offset in array
      );
  
      int last_tex = -1;
      int last_light = -1;
      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);
  
      for (u32 di = 0; di < lev_bucket.next_free_draw; di++) {
        auto& draw = lev_bucket.draws[di];
        glUniform1i(m_uniforms.ignore_alpha, draw.ignore_alpha);
        if ((int)draw.texture != last_tex) {
          glBindTexture(GL_TEXTURE_2D, lev->textures.at(draw.texture));
          last_tex = draw.texture;
        }
  
        if ((int)draw.light_idx != last_light) {
          set_uniform(m_uniforms.light_direction[0], m_lights_buffer[draw.light_idx].direction0);
          set_uniform(m_uniforms.light_direction[1], m_lights_buffer[draw.light_idx].direction1);
          set_uniform(m_uniforms.light_direction[2], m_lights_buffer[draw.light_idx].direction2);
          set_uniform(m_uniforms.light_color[0], m_lights_buffer[draw.light_idx].color0);
          set_uniform(m_uniforms.light_color[1], m_lights_buffer[draw.light_idx].color1);
          set_uniform(m_uniforms.light_color[2], m_lights_buffer[draw.light_idx].color2);
          set_uniform(m_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(m_uniforms.decal, draw.mode.get_decal());
  
        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));
      }
    }
  
    m_next_free_light = 0;
    m_next_free_bone_vector = 0;
    m_next_free_level_bucket = 0;
  }
  