package renderer import "core:mem" import "core:log" import glsl "core:math/linalg/glsl" import bk "../backend" import "../resource" MAX_3D_BILLBOARD_QUADS :: 4096 // Generic world-space billboard batch. Buffer offsets are frame-local so multiple // flushes in one command buffer do not overwrite earlier draws. Billboard_3D_Batch :: struct { vertex_buffers: [bk.MAX_FRAMES_IN_FLIGHT]bk.Buffer_Handle, index_buffers: [bk.MAX_FRAMES_IN_FLIGHT]bk.Buffer_Handle, vertices: [dynamic]Draw2D_Vertex, indices: [dynamic]u32, vertex_buf_offset: int, index_buf_offset: int, parent_state: ^Renderer_State, } // --- Init / Shutdown --- init_billboard_3d_batch :: proc(batch: ^Billboard_3D_Batch, b: ^bk.Backend) -> bool { for i in 0.. bool { if batch == nil || size <= 0 { return false } if batch.vertex_buf_offset + len(batch.vertices) + 4 > MAX_3D_BILLBOARD_QUADS * 4 || batch.index_buf_offset + len(batch.indices) + 6 > MAX_3D_BILLBOARD_QUADS * 6 { log.warn("gpu/renderer: billboard batch full, billboard dropped") return false } fwd: [3]f32 fwd[0] = camera_right[1] * camera_up[2] - camera_right[2] * camera_up[1] fwd[1] = camera_right[2] * camera_up[0] - camera_right[0] * camera_up[2] fwd[2] = camera_right[0] * camera_up[1] - camera_right[1] * camera_up[0] s := size * 0.5 rx := camera_right[0] * s ry := camera_right[1] * s rz := camera_right[2] * s ux := camera_up[0] * s uy := camera_up[1] * s uz := camera_up[2] * s px := position[0] py := position[1] pz := position[2] base := u32(len(batch.vertices)) append(&batch.vertices, Draw2D_Vertex{position = {px - rx - ux, py - ry - uy, pz - rz - uz}, normal = fwd, tex_coord = {0, 1}, color = color}, Draw2D_Vertex{position = {px + rx - ux, py + ry - uy, pz + rz - uz}, normal = fwd, tex_coord = {1, 1}, color = color}, Draw2D_Vertex{position = {px + rx + ux, py + ry + uy, pz + rz + uz}, normal = fwd, tex_coord = {1, 0}, color = color}, Draw2D_Vertex{position = {px - rx + ux, py - ry + uy, pz - rz + uz}, normal = fwd, tex_coord = {0, 0}, color = color}, ) append(&batch.indices, base, base + 1, base + 2, base + 2, base + 3, base) return true } add_quad_3d :: proc( batch: ^Billboard_3D_Batch, corners: [4][3]f32, normal: [3]f32, color: [4]f32, ) -> bool { if batch == nil { return false } if batch.vertex_buf_offset + len(batch.vertices) + 4 > MAX_3D_BILLBOARD_QUADS * 4 || batch.index_buf_offset + len(batch.indices) + 6 > MAX_3D_BILLBOARD_QUADS * 6 { log.warn("gpu/renderer: 3D quad batch full, quad dropped") return false } base := u32(len(batch.vertices)) append(&batch.vertices, Draw2D_Vertex{position = corners[0], normal = normal, tex_coord = {0, 1}, color = color}, Draw2D_Vertex{position = corners[1], normal = normal, tex_coord = {1, 1}, color = color}, Draw2D_Vertex{position = corners[2], normal = normal, tex_coord = {1, 0}, color = color}, Draw2D_Vertex{position = corners[3], normal = normal, tex_coord = {0, 0}, color = color}, ) append(&batch.indices, base, base + 1, base + 2, base + 2, base + 3, base) return true } flush_billboard_3d_batch :: proc( batch: ^Billboard_3D_Batch, b: ^bk.Backend, ctx: bk.Frame_Context, pipeline: bk.Pipeline_Handle, frame_index: u32, proj_view: glsl.mat4x4, res_state: ^resource.Resource_State, ) { if pipeline == bk.NULL_PIPELINE { log.warn("gpu/renderer: skipping 3D billboard flush without pipeline") return } vert_count := len(batch.vertices) idx_count := len(batch.indices) if vert_count == 0 || idx_count == 0 { return } if batch.vertex_buf_offset + vert_count > MAX_3D_BILLBOARD_QUADS * 4 || batch.index_buf_offset + idx_count > MAX_3D_BILLBOARD_QUADS * 6 { log.warn("gpu/renderer: billboard batch frame buffer exhausted, flush skipped") clear(&batch.vertices) clear(&batch.indices) return } // Copy to mapped GPU buffers vb_mapped := b.get_buffer_mapped(batch.vertex_buffers[frame_index]) vb_dst := rawptr(uintptr(vb_mapped) + uintptr(batch.vertex_buf_offset * VERTEX_SIZE)) mem.copy(vb_dst, raw_data(batch.vertices), vert_count * VERTEX_SIZE) ib_mapped := b.get_buffer_mapped(batch.index_buffers[frame_index]) ib_dst := rawptr(uintptr(ib_mapped) + uintptr(batch.index_buf_offset * size_of(u32))) mem.copy(ib_dst, raw_data(batch.indices), idx_count * size_of(u32)) // Bind pipeline (double-sided / no-cull for billboards) b.bind_graphics_pipeline(ctx, pipeline) if batch.parent_state != nil { batch.parent_state.profile.pipeline_binds += 1 } // Bind white texture ds := resource.get_texture_descriptor_set(res_state, 0) b.bind_descriptor_set(ctx, pipeline, ds, 0) if batch.parent_state != nil { batch.parent_state.profile.descriptor_binds += 1 } // Push constants: proj_view + identity model identity: glsl.mat4x4 identity[0, 0] = 1; identity[1, 1] = 1; identity[2, 2] = 1; identity[3, 3] = 1 pc := Push_Constants_3D{ proj_view = proj_view, model = identity, } b.push_constants(ctx, pipeline, {.Vertex}, 0, size_of(Push_Constants_3D), &pc) if batch.parent_state != nil { batch.parent_state.profile.push_constants += 1 } // Bind buffers and draw b.bind_vertex_buffer(ctx, batch.vertex_buffers[frame_index]) b.bind_index_buffer(ctx, batch.index_buffers[frame_index]) b.draw_indexed(ctx, u32(idx_count), 1, u32(batch.index_buf_offset), i32(batch.vertex_buf_offset), 0) if batch.parent_state != nil { batch.parent_state.profile.draw_indexed_calls += 1 batch.parent_state.profile.vertices_submitted += u64(vert_count) batch.parent_state.profile.indices_submitted += u64(idx_count) } batch.vertex_buf_offset += vert_count batch.index_buf_offset += idx_count // Clear for next frame clear(&batch.vertices) clear(&batch.indices) }