Harbor

branch main
showing the latest snapshot on main
renderer.odin 7.6 KB · Plain text
renderer/renderer.odin 0644 Raw
package renderer

import "core:log"
import bk "../backend"
import ir "../render_ir"
import "../profile"
import "../resource"

Renderer_State :: struct {
	frame_active:    bool,
	in_3d_mode:      bool,
	batch_2d:        Batch_2D,
	draw_queue_3d:   Draw_Queue_3D,
	depth_buffer:    Depth_Buffer,
	pipeline_3d:              [bk.MAX_LAYOUT_VARIANTS]bk.Pipeline_Handle,
	pipeline_3d_double_sided: [bk.MAX_LAYOUT_VARIANTS]bk.Pipeline_Handle,
	builtin_cube:   u32,
	builtin_sphere: u32,
	builtin_plane:  u32,
	grid_mesh:      u32,
	grid_slices:    i32,
	grid_spacing:   f32,
	grid_valid:     bool,
	// Lighting
	light_state:                  Light_State,
	pipeline_3d_lit:              [bk.MAX_LAYOUT_VARIANTS]bk.Pipeline_Handle,
	pipeline_3d_lit_double_sided: [bk.MAX_LAYOUT_VARIANTS]bk.Pipeline_Handle,
	pipeline_2d_lit:              bk.Pipeline_Handle,
	lighting_enabled_3d:          bool,
	lighting_enabled_2d:          bool,
	shadows_enabled:              bool,
	camera_pos_3d:                [3]f32,
	camera_target_3d:             [3]f32,
	// Shadows
	shadow_state:                          Shadow_State,
	pipeline_3d_lit_shadowed:              [bk.MAX_LAYOUT_VARIANTS]bk.Pipeline_Handle,
	pipeline_3d_lit_shadowed_double_sided: [bk.MAX_LAYOUT_VARIANTS]bk.Pipeline_Handle,
	// Generic 3D billboards
	billboard_3d_batch: Billboard_3D_Batch,
	camera_right:      [3]f32,
	camera_up:         [3]f32,
	// Deferred render pass state
	render_pass_begun:    bool,
	deferred_clear_color: [4]f32,
	// Per-frame render IR scratch used by incrementally migrated frontend paths.
	frame_ir: ir.Frame_IR,
	profile:  profile.Counters,
	// Backend reference + frame context
	backend:   ^bk.Backend,
	frame_ctx: bk.Frame_Context,
}

init_renderer :: proc(b: ^bk.Backend, res_state: ^resource.Resource_State) -> (state: Renderer_State, ok: bool) {
	state.backend = b

	extent := b.get_extent()

	// Create depth buffer
	db, db_ok := create_depth_buffer(b, extent.width, extent.height)
	if !db_ok {
		return {}, false
	}
	state.depth_buffer = db

	// Create 2D batch
	batch, batch_ok := init_batch_2d(b, res_state)
	if !batch_ok {
		destroy_depth_buffer(b, &state.depth_buffer)
		return {}, false
	}
	state.batch_2d = batch

	// Initialize 3D draw queue
	state.draw_queue_3d.resource_state = res_state

	// Create builtin meshes (PNU layout — 32 bytes/vertex, no vertex color)
	cube_verts, cube_idxs := resource.gen_cube_geometry(1, 1, 1)
	defer delete(cube_verts)
	defer delete(cube_idxs)
	cube_id, _, cube_ok := resource.upload_mesh_raw(
		res_state, b,
		raw_data(cube_verts), len(cube_verts) * size_of(resource.Mesh_Vertex_PNU),
		i32(len(cube_verts)), cube_idxs, bk.LAYOUT_POS_NORM_UV,
	)
	if cube_ok {
		state.builtin_cube = cube_id
	} else {
		log.warn("gpu/renderer: failed to create builtin cube mesh")
	}

	sphere_verts, sphere_idxs := resource.gen_sphere_geometry(1, 16, 32)
	defer delete(sphere_verts)
	defer delete(sphere_idxs)
	sphere_id, _, sphere_ok := resource.upload_mesh_raw(
		res_state, b,
		raw_data(sphere_verts), len(sphere_verts) * size_of(resource.Mesh_Vertex_PNU),
		i32(len(sphere_verts)), sphere_idxs, bk.LAYOUT_POS_NORM_UV,
	)
	if sphere_ok {
		state.builtin_sphere = sphere_id
	} else {
		log.warn("gpu/renderer: failed to create builtin sphere mesh")
	}

	plane_verts, plane_idxs := resource.gen_plane_geometry(1, 1, 1, 1)
	defer delete(plane_verts)
	defer delete(plane_idxs)
	plane_id, _, plane_ok := resource.upload_mesh_raw(
		res_state, b,
		raw_data(plane_verts), len(plane_verts) * size_of(resource.Mesh_Vertex_PNU),
		i32(len(plane_verts)), plane_idxs, bk.LAYOUT_POS_NORM_UV,
	)
	if plane_ok {
		state.builtin_plane = plane_id
	} else {
		log.warn("gpu/renderer: failed to create builtin plane mesh")
	}

	if !init_billboard_3d_batch(&state.billboard_3d_batch, b) {
		log.warn("gpu/renderer: failed to create 3D billboard batch buffers")
	}

	// Initialize lighting system
	if !init_light_state(&state.light_state, b) {
		log.warn("gpu/renderer: failed to initialize lighting state")
	}

	state.frame_ir = ir.init_frame_ir()

	return state, true
}

shutdown_renderer :: proc(state: ^Renderer_State, b: ^bk.Backend) {
	ir.destroy_frame_ir(&state.frame_ir)
	shutdown_billboard_3d_batch(&state.billboard_3d_batch, b)
	for v in 0..<bk.MAX_LAYOUT_VARIANTS {
		b.destroy_graphics_pipeline(state.pipeline_3d_lit_shadowed[v])
		b.destroy_graphics_pipeline(state.pipeline_3d_lit_shadowed_double_sided[v])
	}
	shutdown_shadow_state(&state.shadow_state, b)
	for v in 0..<bk.MAX_LAYOUT_VARIANTS {
		b.destroy_graphics_pipeline(state.pipeline_3d_lit[v])
		b.destroy_graphics_pipeline(state.pipeline_3d_lit_double_sided[v])
	}
	b.destroy_graphics_pipeline(state.pipeline_2d_lit)
	shutdown_light_state(&state.light_state, b)
	if state.grid_valid {
		resource.unload_mesh(state.draw_queue_3d.resource_state, b, state.grid_mesh)
		state.grid_valid = false
	}
	for v in 0..<bk.MAX_LAYOUT_VARIANTS {
		b.destroy_graphics_pipeline(state.pipeline_3d[v])
		b.destroy_graphics_pipeline(state.pipeline_3d_double_sided[v])
	}
	shutdown_batch_2d(&state.batch_2d, b)
	destroy_depth_buffer(b, &state.depth_buffer)
	state.frame_active = false
}

on_swapchain_recreated :: proc(state: ^Renderer_State) {
	extent := state.backend.get_extent()

	// Recreate depth buffer at new dimensions
	recreate_depth_buffer(state.backend, &state.depth_buffer, extent.width, extent.height)

	update_screen_projection(&state.batch_2d, f32(extent.width), f32(extent.height))

	// Recreate depth prepass at new screen size
	on_shadow_resize(&state.shadow_state, state.backend, extent.width, extent.height)
}

// Begin a new frame.
// Render pass is deferred until first draw operation (allows compute before render pass).
begin_frame :: proc(state: ^Renderer_State, clear_color: [4]f32) {
	prof_start := profile.now()
	ctx, ok := state.backend.begin_frame(clear_color)
	if !ok {
		return
	}
	state.profile.frame_begin_ns += profile.elapsed_ns(prof_start, profile.now())
	state.profile.frame_count += 1
	state.frame_ctx = ctx

	state.deferred_clear_color = clear_color
	state.render_pass_begun = false

	reset_batch_2d(&state.batch_2d)
	reset_draw_queue_3d(&state.draw_queue_3d)
	reset_billboard_3d_batch(&state.billboard_3d_batch)
	begin_batch_frame(&state.batch_2d, state.backend, ctx)
	state.batch_2d.parent_state = state
	state.billboard_3d_batch.parent_state = state
	state.frame_active = true
	state.in_3d_mode = false
}

// Lazily start the render pass. Called before any draw operation.
ensure_render_pass :: proc(state: ^Renderer_State) {
	if state.render_pass_begun || !state.frame_active {
		return
	}

	prof_start := profile.now()
	state.backend.begin_default_pass(state.frame_ctx, state.deferred_clear_color)

	extent := state.backend.get_extent()
	state.backend.set_viewport(state.frame_ctx, 0, 0, f32(extent.width), f32(extent.height))
	state.backend.set_scissor(state.frame_ctx, 0, 0, extent.width, extent.height)
	state.profile.render_pass_begin_ns += profile.elapsed_ns(prof_start, profile.now())

	state.render_pass_begun = true
}

// End frame: end render pass, submit command buffer, present.
// Returns true if swapchain was recreated.
end_frame :: proc(state: ^Renderer_State) -> bool {
	if !state.frame_active {
		return false
	}

	prof_start := profile.now()

	// Flush 2D batch before ending render pass
	flush_batch_2d(&state.batch_2d)

	// Ensure render pass was started (even if no draws happened)
	ensure_render_pass(state)

	// End render pass
	state.backend.end_render_pass(state.frame_ctx)

	// End frame and present
	resized := state.backend.end_frame(state.frame_ctx)
	state.profile.frame_end_ns += profile.elapsed_ns(prof_start, profile.now())

	state.frame_active = false

	if resized {
		on_swapchain_recreated(state)
		return true
	}

	return false
}