Harbor

branch main
showing the latest snapshot on main
particles.odin 7.3 KB · Plain text
gpu/renderer/particles.odin 0644 Raw
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..<bk.MAX_FRAMES_IN_FLIGHT {
		vb_handle, vb_ok := b.create_buffer(bk.Buffer_Desc{
			size   = u64(MAX_3D_BILLBOARD_QUADS * 4 * VERTEX_SIZE),
			usage  = {.Vertex},
			memory = {.Host_Visible, .Host_Coherent},
		})
		if !vb_ok {
			shutdown_billboard_3d_batch(batch, b)
			return false
		}
		b.map_buffer(vb_handle)
		batch.vertex_buffers[i] = vb_handle

		ib_handle, ib_ok := b.create_buffer(bk.Buffer_Desc{
			size   = u64(MAX_3D_BILLBOARD_QUADS * 6 * size_of(u32)),
			usage  = {.Index},
			memory = {.Host_Visible, .Host_Coherent},
		})
		if !ib_ok {
			shutdown_billboard_3d_batch(batch, b)
			return false
		}
		b.map_buffer(ib_handle)
		batch.index_buffers[i] = ib_handle
	}

	batch.vertices = make([dynamic]Draw2D_Vertex, 0, MAX_3D_BILLBOARD_QUADS * 4)
	batch.indices = make([dynamic]u32, 0, MAX_3D_BILLBOARD_QUADS * 6)
	return true
}

shutdown_billboard_3d_batch :: proc(batch: ^Billboard_3D_Batch, b: ^bk.Backend) {
	for i in 0..<bk.MAX_FRAMES_IN_FLIGHT {
		if batch.vertex_buffers[i] != bk.NULL_BUFFER {
			b.unmap_buffer(batch.vertex_buffers[i])
			b.destroy_buffer(batch.vertex_buffers[i])
			batch.vertex_buffers[i] = bk.NULL_BUFFER
		}

		if batch.index_buffers[i] != bk.NULL_BUFFER {
			b.unmap_buffer(batch.index_buffers[i])
			b.destroy_buffer(batch.index_buffers[i])
			batch.index_buffers[i] = bk.NULL_BUFFER
		}
	}
	delete(batch.vertices)
	delete(batch.indices)
}

reset_billboard_3d_batch :: proc(batch: ^Billboard_3D_Batch) {
	clear(&batch.vertices)
	clear(&batch.indices)
	batch.vertex_buf_offset = 0
	batch.index_buf_offset = 0
}

add_billboard_3d :: proc(
	batch: ^Billboard_3D_Batch,
	position: [3]f32,
	size: f32,
	color: [4]f32,
	camera_right: [3]f32,
	camera_up: [3]f32,
) -> 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)
}