package gpu import "core:log" import "core:math" import "core:mem" import "core:os" import glsl "core:math/linalg/glsl" import bk "backend" import vk_backend "backend/vulkan" import d3d11_backend "backend/d3d11" import d3d12_backend "backend/d3d12" import opengl_backend "backend/opengl" import "compiler" import "renderer" import ir "render_ir" import prof "profile" import "resource" // Internal engine state -- global singleton like Raylib @(private) g_state: ^Engine_State @(private) Engine_State :: struct { // Backend backend: bk.Backend, // Resources resource_state: resource.Resource_State, // Rendering render_state: renderer.Renderer_State, offscreen_targets: compiler.Render_Target_Cache, offscreen_2d_pipelines: [dynamic]Offscreen_2D_Pipeline, offscreen_sampled_views: [dynamic]Offscreen_Sampled_View, pipeline_2d_vertex_shader: bk.Shader_Handle, pipeline_2d_fragment_shader: bk.Shader_Handle, // Memory arena: mem.Arena, arena_buffer: []byte, // Camera shake shake_2d: Camera_Shake_State, shake_3d: Camera_Shake_State, // State initialized: bool, headless: bool, } @(private) Offscreen_2D_Pipeline :: struct { render_pass: bk.Render_Pass_Handle, pipeline: bk.Pipeline_Handle, } @(private) Offscreen_Sampled_View :: struct { texture: bk.Texture_Handle, texture_id: u32, width: u32, height: u32, } @(private) Camera_Shake_State :: struct { trauma: f32, // 0..1 current intensity, decays over time decay: f32, // trauma units per second max_offset: f32, // max pixel/unit offset max_angle: f32, // max rotation degrees (2D only) seed: f32, // time accumulator for noise } // --- Lifecycle --- // Initialize GPU resources for an externally-owned presentation surface. init_surface :: proc(surface: Surface_Desc, width, height: u32, title: cstring = "GPU") -> bool { if g_state != nil && g_state.initialized { log.error("gpu: already initialized") return false } state := new(Engine_State) if state == nil { log.error("gpu: failed to allocate engine state") return false } g_state = state if !init_gpu_and_renderer(state, surface, width, height, title) { return false } state.initialized = true return true } // Initialize only the selected backend. This is for proof/offscreen runtimes // that drive compiler.Execution_State directly and do not need app resources. init_headless :: proc(width, height: u32, title: cstring = "GPU headless") -> bool { if g_state != nil && g_state.initialized { log.error("gpu: already initialized") return false } state := new(Engine_State) if state == nil { log.error("gpu: failed to allocate engine state") return false } g_state = state if !init_backend(state, {}, width, height, title) { return false } state.initialized = true state.headless = true return true } // Shared backend initialization. @(private) init_backend :: proc(state: ^Engine_State, surface: Surface_Desc, width, height: u32, title: cstring) -> bool { when bk.GPU_BACKEND == "vulkan" { backend, backend_ok := vk_backend.init_vulkan_backend(surface, width, height, title) if !backend_ok { log.error("gpu: failed to initialize Vulkan backend") free(state) g_state = nil return false } state.backend = backend } else when bk.GPU_BACKEND == "d3d11" { backend, backend_ok := d3d11_backend.init_d3d11_backend(surface, width, height, title) if !backend_ok { log.error("gpu: failed to initialize D3D11 backend") free(state) g_state = nil return false } state.backend = backend } else when bk.GPU_BACKEND == "d3d12" { backend, backend_ok := d3d12_backend.init_d3d12_backend(surface, width, height, title) if !backend_ok { log.error("gpu: failed to initialize D3D12 backend") free(state) g_state = nil return false } state.backend = backend } else when bk.GPU_BACKEND == "opengl" { backend, backend_ok := opengl_backend.init_opengl_backend(surface, width, height, title) if !backend_ok { log.error("gpu: failed to initialize OpenGL backend") free(state) g_state = nil return false } state.backend = backend } else { #assert(false, "unsupported GPU_BACKEND") } if !bk.backend_vtable_is_complete(&state.backend) { log.error("gpu: backend vtable is incomplete") if state.backend.shutdown != nil { state.backend.shutdown() } free(state) g_state = nil return false } return true } // Shared GPU + renderer initialization. @(private) init_gpu_and_renderer :: proc(state: ^Engine_State, surface: Surface_Desc, width, height: u32, title: cstring) -> bool { if !init_backend(state, surface, width, height, title) { return false } // Init resources res, res_ok := resource.init_resources(&state.backend) if !res_ok { log.error("gpu: failed to initialize resources") shutdown() return false } state.resource_state = res // Init renderer rs, rs_ok := renderer.init_renderer(&state.backend, &state.resource_state) if !rs_ok { log.error("gpu: failed to initialize renderer") shutdown() return false } state.render_state = rs state.render_state.batch_2d.parent_state = &state.render_state state.offscreen_targets = compiler.init_render_target_cache(&state.backend) state.offscreen_2d_pipelines = make([dynamic]Offscreen_2D_Pipeline, 0, ir.DEFAULT_PASS_CAP) state.offscreen_sampled_views = make([dynamic]Offscreen_Sampled_View, 0, ir.DEFAULT_PASS_CAP) // Camera shake defaults state.shake_2d = Camera_Shake_State{decay = 1.0, max_offset = 10.0, max_angle = 5.0} state.shake_3d = Camera_Shake_State{decay = 1.0, max_offset = 0.3} return true } // Shut down GPU resources. shutdown :: proc() { if g_state == nil { return } g_state.backend.wait_idle() if !g_state.headless { destroy_offscreen_2d_pipelines() destroy_offscreen_sampled_views() compiler.destroy_render_target_cache(&g_state.offscreen_targets) renderer.shutdown_renderer(&g_state.render_state, &g_state.backend) resource.shutdown_resources(&g_state.resource_state, &g_state.backend) } g_state.backend.shutdown() free(g_state) g_state = nil } // Get current surface width. get_screen_width :: proc() -> i32 { if g_state == nil || !g_state.initialized { return 0 } return i32(g_state.backend.get_extent().width) } // Get current surface height. get_screen_height :: proc() -> i32 { if g_state == nil || !g_state.initialized { return 0 } return i32(g_state.backend.get_extent().height) } resize_surface :: proc(width, height: u32) { if g_state == nil || !g_state.initialized { return } g_state.backend.on_resize(width, height) renderer.on_swapchain_recreated(&g_state.render_state) } // --- Drawing --- // Begin frame: acquire swap image and begin command buffer. begin_drawing :: proc(clear_color: Color = BLACK, dt: f32 = 0) { if g_state == nil || !g_state.initialized { return } // Decay camera shake if g_state.shake_2d.trauma > 0 { g_state.shake_2d.seed += dt * 60 g_state.shake_2d.trauma = max(0, g_state.shake_2d.trauma - g_state.shake_2d.decay * dt) } if g_state.shake_3d.trauma > 0 { g_state.shake_3d.seed += dt * 60 g_state.shake_3d.trauma = max(0, g_state.shake_3d.trauma - g_state.shake_3d.decay * dt) } renderer.begin_frame(&g_state.render_state, clear_color) } // End frame: end command buffer, submit, present. end_drawing :: proc() -> bool { if g_state == nil || !g_state.initialized { return false } resized := renderer.end_frame(&g_state.render_state) return resized } write_profile :: proc(path: string, app_wall_seconds: f64 = -1, app_frame_count: u64 = 0) -> bool { if g_state == nil || len(path) == 0 { return false } if app_wall_seconds >= 0 { g_state.render_state.profile.app_wall_seconds = app_wall_seconds g_state.render_state.profile.app_frame_count = app_frame_count if g_state.headless && g_state.render_state.profile.frame_count == 0 { g_state.render_state.profile.frame_count = app_frame_count } } data := prof.to_json(g_state.render_state.profile, context.temp_allocator) err := os.write_entire_file(path, data) return err == nil } // --- 2D Drawing --- begin_mode_2d :: proc(camera: Camera2D = {}) { if g_state == nil || !g_state.render_state.frame_active { return } // Flush current batch before changing projection renderer.flush_batch_2d(&g_state.render_state.batch_2d) renderer.reset_batch_2d(&g_state.render_state.batch_2d) extent := g_state.backend.get_extent() w := f32(extent.width) h := f32(extent.height) proj := renderer.ortho_2d(0, w, 0, h) cam := camera if cam.zoom == 0 { cam.zoom = 1 } // Build view: translate(offset) * scale(zoom) * rotate(-rotation) * translate(-target) rot_rad := -cam.rotation * math.PI / 180.0 cos_r := math.cos(rot_rad) sin_r := math.sin(rot_rad) // Odin matrix: m[row, col] view: glsl.mat4x4 view[0, 0] = cam.zoom * cos_r view[0, 1] = cam.zoom * sin_r view[1, 0] = -cam.zoom * sin_r view[1, 1] = cam.zoom * cos_r view[2, 2] = 1 view[3, 3] = 1 // Apply translate(-target) by adjusting the translation column (col 3) tx := -cam.target.x * view[0, 0] + -cam.target.y * view[1, 0] + cam.offset.x ty := -cam.target.x * view[0, 1] + -cam.target.y * view[1, 1] + cam.offset.y view[0, 3] = tx view[1, 3] = ty // Apply camera shake if g_state.shake_2d.trauma > 0 { shake := g_state.shake_2d.trauma * g_state.shake_2d.trauma s := &g_state.shake_2d ox := shake * s.max_offset * shake_noise(s.seed) oy := shake * s.max_offset * shake_noise(s.seed + 100) ar := shake * s.max_angle * shake_noise(s.seed + 200) * math.PI / 180.0 // Apply shake as additional rotation and offset sc := math.cos(ar) ss := math.sin(ar) view[0, 3] += ox view[1, 3] += oy // Compose small rotation r00 := view[0, 0]; r01 := view[0, 1] r10 := view[1, 0]; r11 := view[1, 1] view[0, 0] = r00 * sc + r10 * ss view[0, 1] = r01 * sc + r11 * ss view[1, 0] = r10 * sc - r00 * ss view[1, 1] = r11 * sc - r01 * ss } renderer.set_projection_2d(&g_state.render_state.batch_2d, proj * view) // Set up 2D lit rendering if enabled if g_state.render_state.lighting_enabled_2d { g_state.render_state.batch_2d.use_lit = true g_state.render_state.batch_2d.lit_pipeline = g_state.render_state.pipeline_2d_lit g_state.render_state.batch_2d.light_desc_set = g_state.render_state.light_state.ubo_descriptor_sets[g_state.render_state.frame_ctx.frame_index] // Update light UBO for 2D (camera_pos not relevant for 2D) renderer.update_light_ubo( &g_state.render_state.light_state, &g_state.backend, g_state.render_state.frame_ctx.frame_index, proj * view, {0, 0, 0}, ) } else { g_state.render_state.batch_2d.use_lit = false } } end_mode_2d :: proc() { if g_state == nil || !g_state.render_state.frame_active { return } // Flush current batch before restoring projection renderer.flush_batch_2d(&g_state.render_state.batch_2d) renderer.reset_batch_2d(&g_state.render_state.batch_2d) renderer.reset_projection_2d(&g_state.render_state.batch_2d) } measure_text :: proc(text: string, scale: f32 = 1.0) -> (width, height: f32) { return renderer.text_width(text, scale), renderer.text_height(text, scale) } // --- 3D Drawing --- GRID_Y_OFFSET_3D :: f32(0.01) // --- Resources --- load_texture :: proc(path: cstring) -> Texture { if g_state == nil || !g_state.initialized { return {} } id, w, h, ok := resource.load_texture_from_file( &g_state.resource_state, &g_state.backend, string(path), ) if !ok { return {} } return Texture{id = id, width = w, height = h} } load_texture_from_rgba :: proc(pixels: []u8, width, height: i32) -> Texture { if g_state == nil || !g_state.initialized { return {} } id, ok := resource.load_texture_from_pixels( &g_state.resource_state, &g_state.backend, raw_data(pixels), u32(width), u32(height), ) if !ok { return {} } return Texture{id = id, width = width, height = height} } unload_texture :: proc(tex: Texture) { if g_state == nil { return } g_state.backend.wait_idle() resource.unload_texture(&g_state.resource_state, &g_state.backend, tex.id) } gen_mesh_cube :: proc(w, h, l: f32) -> Mesh { if g_state == nil || !g_state.initialized { return {} } verts, idxs := resource.gen_cube_geometry(w, h, l) defer delete(verts) defer delete(idxs) id, ic, ok := resource.upload_mesh_raw( &g_state.resource_state, &g_state.backend, raw_data(verts), len(verts) * size_of(resource.Mesh_Vertex_PNU), i32(len(verts)), idxs, bk.LAYOUT_POS_NORM_UV, ) if !ok { return {} } return Mesh{id = id, vertex_count = i32(len(verts)), index_count = ic} } gen_mesh_sphere :: proc(radius: f32, rings, slices: i32) -> Mesh { if g_state == nil || !g_state.initialized { return {} } verts, idxs := resource.gen_sphere_geometry(radius, rings, slices) defer delete(verts) defer delete(idxs) id, ic, ok := resource.upload_mesh_raw( &g_state.resource_state, &g_state.backend, raw_data(verts), len(verts) * size_of(resource.Mesh_Vertex_PNU), i32(len(verts)), idxs, bk.LAYOUT_POS_NORM_UV, ) if !ok { return {} } return Mesh{id = id, vertex_count = i32(len(verts)), index_count = ic} } gen_mesh_plane :: proc(w, l: f32, res_x, res_z: i32) -> Mesh { if g_state == nil || !g_state.initialized { return {} } verts, idxs := resource.gen_plane_geometry(w, l, res_x, res_z) defer delete(verts) defer delete(idxs) id, ic, ok := resource.upload_mesh_raw( &g_state.resource_state, &g_state.backend, raw_data(verts), len(verts) * size_of(resource.Mesh_Vertex_PNU), i32(len(verts)), idxs, bk.LAYOUT_POS_NORM_UV, ) if !ok { return {} } return Mesh{id = id, vertex_count = i32(len(verts)), index_count = ic} } gen_mesh_custom :: proc(vertices: []Vertex, indices: []u32) -> Mesh { if g_state == nil || !g_state.initialized { return {} } id, ic, ok := resource.upload_mesh_raw( &g_state.resource_state, &g_state.backend, raw_data(vertices), len(vertices) * size_of(Vertex), i32(len(vertices)), indices, bk.LAYOUT_POS_NORM_UV_COLOR, ) if !ok { return {} } return Mesh{id = id, vertex_count = i32(len(vertices)), index_count = ic} } gen_mesh_raw :: proc(vertex_data: rawptr, vertex_data_size: int, vertex_count: i32, indices: []u32, layout: Vertex_Layout) -> Mesh { if g_state == nil || !g_state.initialized { return {} } id, ic, ok := resource.upload_mesh_raw( &g_state.resource_state, &g_state.backend, vertex_data, vertex_data_size, vertex_count, indices, layout, ) if !ok { return {} } return Mesh{id = id, vertex_count = vertex_count, index_count = ic} } get_mesh_layout :: proc(mesh_id: u32) -> (Vertex_Layout, bool) { if g_state == nil { return {}, false } return resource.get_mesh_layout(&g_state.resource_state, mesh_id) } unload_mesh :: proc(mesh: Mesh) { if g_state == nil { return } g_state.backend.wait_idle() resource.unload_mesh(&g_state.resource_state, &g_state.backend, mesh.id) } load_model :: proc(path: cstring) -> Model { if g_state == nil || !g_state.initialized { return {} } id, mc, ok := resource.load_model_from_gltf( &g_state.resource_state, &g_state.backend, string(path), ) if !ok { return {} } internal := &g_state.resource_state.models[id] meshes := make([]Mesh, mc) materials := make([]Material, mc) for i in 0.. Shader { // TODO: File-based shader loading return {} } unload_shader :: proc(shader: Shader) { // TODO: File-based shader unloading } // --- Pipeline API --- create_shader_module :: proc(desc: Shader_Module_Desc) -> (Shader_Handle, bool) { if g_state == nil { return {}, false } return g_state.backend.create_shader_module(desc) } destroy_shader :: proc(handle: Shader_Handle) { if g_state == nil { return } g_state.backend.destroy_shader(handle) } // Create a graphics pipeline from a descriptor. create_pipeline :: proc(desc: Pipeline_Desc) -> (Pipeline_Handle, bool) { if g_state == nil { return {}, false } if !bk.supports_push_constant_size(g_state.backend.capabilities, desc.push_constant_size) { log.errorf( "gpu: graphics pipeline push constant size %d exceeds backend implemented limit %d", desc.push_constant_size, g_state.backend.capabilities.max_push_constant_size, ) return {}, false } return g_state.backend.create_graphics_pipeline(desc) } destroy_pipeline :: proc(handle: Pipeline_Handle) { if g_state == nil { return } g_state.backend.destroy_graphics_pipeline(handle) } // Query the default render pass for pipeline creation. get_default_render_pass :: proc() -> Render_Pass_Handle { if g_state == nil { return {} } return g_state.backend.get_default_render_pass() } get_backend :: proc() -> ^Backend { if g_state == nil || !g_state.initialized { return nil } return &g_state.backend } get_capabilities :: proc() -> Capabilities { if g_state == nil || !g_state.initialized { return bk.no_capabilities() } return g_state.backend.capabilities } supports :: proc(capability: Capability) -> bool { return bk.supports(get_capabilities(), capability) } get_frame_context :: proc() -> Frame_Context { if g_state == nil || !g_state.render_state.frame_active { return {} } return g_state.render_state.frame_ctx } ensure_default_render_pass :: proc() -> bool { if g_state == nil || !g_state.render_state.frame_active { return false } renderer.ensure_render_pass(&g_state.render_state) return g_state.render_state.render_pass_begun } // Query the texture sampler descriptor set layout (set 0). get_texture_descriptor_layout :: proc() -> Descriptor_Handle { if g_state == nil { return {} } return resource.get_descriptor_set_layout(&g_state.resource_state) } // Query the light UBO descriptor set layout (set 1). get_light_descriptor_layout :: proc() -> Descriptor_Handle { if g_state == nil { return {} } return g_state.render_state.light_state.ubo_set_layout } // --- Pipeline Registration --- // Register externally-created pipelines for use by GPU's draw routines. set_pipeline_2d :: proc(pipeline: Pipeline_Handle) { if g_state == nil { return } g_state.render_state.batch_2d.gfx_pipeline = pipeline } set_pipeline_2d_shaders :: proc(vertex_shader, fragment_shader: Shader_Handle) { if g_state == nil { return } g_state.pipeline_2d_vertex_shader = vertex_shader g_state.pipeline_2d_fragment_shader = fragment_shader } destroy_offscreen_2d_pipelines :: proc() { if g_state == nil { return } for entry in g_state.offscreen_2d_pipelines { if entry.pipeline != bk.NULL_PIPELINE { g_state.backend.destroy_graphics_pipeline(entry.pipeline) } } clear(&g_state.offscreen_2d_pipelines) } destroy_offscreen_sampled_views :: proc() { if g_state == nil { return } for entry in g_state.offscreen_sampled_views { if entry.texture_id != 0 { resource.unload_texture(&g_state.resource_state, &g_state.backend, entry.texture_id) } } clear(&g_state.offscreen_sampled_views) } @(private) get_or_import_offscreen_sampled_texture :: proc(runtime: ^Engine_State, texture: bk.Texture_Handle, width, height: u32) -> (u32, bool) { if runtime == nil || texture == bk.NULL_TEXTURE || width == 0 || height == 0 { return 0, false } for entry in runtime.offscreen_sampled_views { if entry.texture == texture && entry.width == width && entry.height == height { return entry.texture_id, true } } texture_id, ok := resource.import_texture_view(&runtime.resource_state, &runtime.backend, texture, width, height) if !ok { return 0, false } append(&runtime.offscreen_sampled_views, Offscreen_Sampled_View{ texture = texture, texture_id = texture_id, width = width, height = height, }) return texture_id, true } @(private) get_or_create_offscreen_2d_pipeline :: proc(render_pass: bk.Render_Pass_Handle) -> (bk.Pipeline_Handle, bool) { if g_state == nil || render_pass == bk.NULL_RENDER_PASS { return bk.NULL_PIPELINE, false } for entry in g_state.offscreen_2d_pipelines { if entry.render_pass == render_pass { return entry.pipeline, entry.pipeline != bk.NULL_PIPELINE } } if g_state.pipeline_2d_vertex_shader == bk.NULL_SHADER || g_state.pipeline_2d_fragment_shader == bk.NULL_SHADER { return bk.NULL_PIPELINE, false } layout := LAYOUT_POS_NORM_UV_COLOR binding, attrs, attr_count := vertex_layout_to_pipeline_attrs(&layout) vertex_bindings := [1]Vertex_Binding{binding} descriptor_layouts := [1]Descriptor_Handle{resource.get_descriptor_set_layout(&g_state.resource_state)} pipeline, ok := g_state.backend.create_graphics_pipeline({ vert_shader = g_state.pipeline_2d_vertex_shader, frag_shader = g_state.pipeline_2d_fragment_shader, render_pass = render_pass, topology = .Triangle_List, cull_mode = .None, front_face = .Counter_Clockwise, enable_blending = true, blend_mode = .Alpha, enable_depth_test = false, push_constant_size = 64, push_constant_stages = {.Vertex}, descriptor_layouts = descriptor_layouts[:], vertex_bindings = vertex_bindings[:], vertex_attributes = attrs[:attr_count], }) if !ok { return bk.NULL_PIPELINE, false } append(&g_state.offscreen_2d_pipelines, Offscreen_2D_Pipeline{render_pass = render_pass, pipeline = pipeline}) return pipeline, true } set_pipeline_2d_lit :: proc(pipeline: Pipeline_Handle) { if g_state == nil { return } g_state.render_state.pipeline_2d_lit = pipeline } set_pipeline_3d :: proc(variant: Layout_Variant, culled, double_sided: Pipeline_Handle) { if g_state == nil { return } g_state.render_state.pipeline_3d[variant] = culled g_state.render_state.pipeline_3d_double_sided[variant] = double_sided } set_pipeline_3d_lit :: proc(variant: Layout_Variant, culled, double_sided: Pipeline_Handle) { if g_state == nil { return } g_state.render_state.pipeline_3d_lit[variant] = culled g_state.render_state.pipeline_3d_lit_double_sided[variant] = double_sided } set_pipeline_3d_lit_shadowed :: proc(variant: Layout_Variant, culled, double_sided: Pipeline_Handle) { if g_state == nil { return } g_state.render_state.pipeline_3d_lit_shadowed[variant] = culled g_state.render_state.pipeline_3d_lit_shadowed_double_sided[variant] = double_sided } // --- Compute --- load_compute_shader :: proc(path: cstring, num_buffers: u32 = 4, push_constant_size: u32 = 0) -> Compute_Shader { if g_state == nil { return {} } id, ok := resource.load_compute_shader_resource( &g_state.resource_state, &g_state.backend, path, num_buffers, push_constant_size, ) if !ok { return {} } return Compute_Shader{id = id} } load_compute_shader_from_bytes :: proc(name: string, data: []u8, format: Shader_Format = REQUIRED_SHADER_FORMAT, num_buffers: u32 = 4, push_constant_size: u32 = 0) -> Compute_Shader { if g_state == nil { return {} } id, ok := resource.create_compute_shader_resource( &g_state.resource_state, &g_state.backend, name, data, format, num_buffers, push_constant_size, ) if !ok { return {} } return Compute_Shader{id = id} } unload_compute_shader :: proc(shader: Compute_Shader) { if g_state == nil { return } g_state.backend.wait_idle() resource.unload_compute_shader_resource(&g_state.resource_state, &g_state.backend, shader.id) } create_storage_buffer :: proc(size: int, data: rawptr = nil) -> Storage_Buffer { if g_state == nil || size <= 0 { return {} } id, ok := resource.create_storage_buffer_resource( &g_state.resource_state, &g_state.backend, size, data, ) if !ok { return {} } return Storage_Buffer{id = id, size = size} } update_storage_buffer :: proc(buf: Storage_Buffer, data: rawptr, size: int, offset: int = 0) { if g_state == nil || data == nil || size <= 0 { return } sb, ok := resource.get_storage_buffer(&g_state.resource_state, buf.id) if !ok { return } mapped := g_state.backend.get_buffer_mapped(sb.buffer) if mapped == nil { return } if offset < 0 || offset + size > sb.size { log.error("gpu: storage buffer write out of bounds") return } dst := rawptr(uintptr(mapped) + uintptr(offset)) mem.copy(dst, data, size) } read_storage_buffer :: proc(buf: Storage_Buffer, dest: rawptr, size: int, offset: int = 0) { if g_state == nil || dest == nil || size <= 0 { return } sb, ok := resource.get_storage_buffer(&g_state.resource_state, buf.id) if !ok { return } mapped := g_state.backend.get_buffer_mapped(sb.buffer) if mapped == nil { return } if offset < 0 || offset + size > sb.size { log.error("gpu: storage buffer read out of bounds") return } src := rawptr(uintptr(mapped) + uintptr(offset)) mem.copy(dest, src, size) } unload_storage_buffer :: proc(buf: Storage_Buffer) { if g_state == nil { return } g_state.backend.wait_idle() resource.unload_storage_buffer_resource(&g_state.resource_state, &g_state.backend, buf.id) } // --- Camera System --- // Easing function: maps t in [0,1] to eased value in [0,1] ease :: proc(t: f32, type: Ease_Type = .LINEAR) -> f32 { s := clamp(t, 0, 1) switch type { case .LINEAR: return s case .SINE_IN: return 1 - math.cos(s * math.PI * 0.5) case .SINE_OUT: return math.sin(s * math.PI * 0.5) case .SINE_IN_OUT: return -(math.cos(s * math.PI) - 1) * 0.5 case .QUAD_IN: return s * s case .QUAD_OUT: return 1 - (1 - s) * (1 - s) case .QUAD_IN_OUT: if s < 0.5 { return 2 * s * s } return 1 - (-2 * s + 2) * (-2 * s + 2) * 0.5 case .CUBIC_IN: return s * s * s case .CUBIC_OUT: inv := 1 - s return 1 - inv * inv * inv case .CUBIC_IN_OUT: if s < 0.5 { return 4 * s * s * s } inv := -2 * s + 2 return 1 - inv * inv * inv * 0.5 case .EXPO_IN: return s == 0 ? 0 : math.pow(2, 10 * (s - 1)) case .EXPO_OUT: return s == 1 ? 1 : 1 - math.pow(2, -10 * s) case .EXPO_IN_OUT: if s == 0 { return 0 } if s == 1 { return 1 } if s < 0.5 { return math.pow(2, 20 * s - 10) * 0.5 } return (2 - math.pow(2, -20 * s + 10)) * 0.5 } return s } // Interpolate between two 2D cameras camera_lerp_2d :: proc(from, to: Camera2D, t: f32) -> Camera2D { s := clamp(t, 0, 1) inv := 1 - s return Camera2D{ offset = from.offset * inv + to.offset * s, target = from.target * inv + to.target * s, rotation = from.rotation * inv + to.rotation * s, zoom = from.zoom * inv + to.zoom * s, } } // Interpolate between two 3D cameras camera_lerp_3d :: proc(from, to: Camera3D, t: f32) -> Camera3D { s := clamp(t, 0, 1) inv := 1 - s up := from.up * inv + to.up * s up_len := math.sqrt(up.x * up.x + up.y * up.y + up.z * up.z) if up_len < 1e-8 { up = {0, 1, 0} } else { up = up / up_len } return Camera3D{ position = from.position * inv + to.position * s, target = from.target * inv + to.target * s, up = up, fovy = from.fovy * inv + to.fovy * s, near = from.near * inv + to.near * s, far = from.far * inv + to.far * s, } } // Smoothly follow a 2D target (exponential decay, frame-rate independent) camera_smooth_follow_2d :: proc(camera: ^Camera2D, target: Vec2, speed, dt: f32) { if camera == nil { return } if dt <= 0 { return } if speed <= 0 { camera.target = target return } factor := 1 - math.exp(-speed * dt) camera.target += (target - camera.target) * factor } // Smoothly follow a 3D target (maintains position-target offset) camera_smooth_follow_3d :: proc(camera: ^Camera3D, target_pos: Vec3, speed, dt: f32) { if camera == nil { return } if dt <= 0 { return } if speed <= 0 { offset := camera.position - camera.target camera.target = target_pos camera.position = target_pos + offset return } offset := camera.position - camera.target factor := 1 - math.exp(-speed * dt) camera.target += (target_pos - camera.target) * factor camera.position = camera.target + offset } // Convert screen position to world position (2D) get_screen_to_world_2d :: proc(screen_pos: Vec2, camera: Camera2D) -> Vec2 { zoom := camera.zoom if camera.zoom != 0 else f32(1) rot_rad := camera.rotation * math.PI / 180.0 cos_r := math.cos(rot_rad) sin_r := math.sin(rot_rad) // Undo offset, undo zoom p := (screen_pos - camera.offset) / zoom // Undo rotation (rotate by +rotation to invert -rotation) return Vec2{ p.x * cos_r - p.y * sin_r + camera.target.x, p.x * sin_r + p.y * cos_r + camera.target.y, } } // Convert world position to screen position (2D) get_world_to_screen_2d :: proc(world_pos: Vec2, camera: Camera2D) -> Vec2 { zoom := camera.zoom if camera.zoom != 0 else f32(1) rot_rad := -camera.rotation * math.PI / 180.0 cos_r := math.cos(rot_rad) sin_r := math.sin(rot_rad) // Translate by -target p := world_pos - camera.target // Rotate by -rotation rotated := Vec2{ p.x * cos_r - p.y * sin_r, p.x * sin_r + p.y * cos_r, } // Scale by zoom, add offset return rotated * zoom + camera.offset } // Project a 3D world position to 2D screen coordinates get_world_to_screen_3d :: proc(world_pos: Vec3, camera: Camera3D) -> Vec2 { view := camera_view_matrix(camera) proj := camera_proj_matrix(camera) pv := proj * view // Multiply proj_view * vec4(world_pos, 1) clip := mat4_mul_vec4(pv, {world_pos.x, world_pos.y, world_pos.z, 1}) if clip.w <= 0 { return {-1, -1} } // Perspective divide ndc_x := clip.x / clip.w ndc_y := clip.y / clip.w // NDC to screen (our perspective already flips Y for Vulkan) w := f32(get_screen_width()) h := f32(get_screen_height()) return Vec2{ (ndc_x + 1) * 0.5 * w, (ndc_y + 1) * 0.5 * h, } } // Unproject a screen position into a world-space ray get_screen_to_world_ray_3d :: proc(screen_pos: Vec2, camera: Camera3D) -> (origin: Vec3, direction: Vec3) { w := f32(get_screen_width()) h := f32(get_screen_height()) if w <= 0 || h <= 0 { return camera.position, {0, 0, -1} } // Screen to NDC ndc_x := screen_pos.x / w * 2 - 1 ndc_y := screen_pos.y / h * 2 - 1 view := camera_view_matrix(camera) proj := camera_proj_matrix(camera) inv := mat4_inverse(proj * view) // Unproject near and far points near_clip := mat4_mul_vec4(inv, {ndc_x, ndc_y, 0, 1}) far_clip := mat4_mul_vec4(inv, {ndc_x, ndc_y, 1, 1}) if math.abs(near_clip.w) < 1e-8 || math.abs(far_clip.w) < 1e-8 { return camera.position, {0, 0, -1} } near_world := Vec3{near_clip.x, near_clip.y, near_clip.z} / near_clip.w far_world := Vec3{far_clip.x, far_clip.y, far_clip.z} / far_clip.w dir := far_world - near_world dir_len := math.sqrt(dir.x * dir.x + dir.y * dir.y + dir.z * dir.z) if dir_len < 1e-8 { return camera.position, {0, 0, -1} } return near_world, dir / dir_len } // Get the view matrix for a 3D camera camera_view_matrix :: proc(camera: Camera3D) -> Mat4 { f := vec3_normalize(camera.target - camera.position) r := vec3_normalize(vec3_cross(f, camera.up)) u := vec3_cross(r, f) m: Mat4 m[0, 0] = r.x; m[0, 1] = r.y; m[0, 2] = r.z m[1, 0] = u.x; m[1, 1] = u.y; m[1, 2] = u.z m[2, 0] = -f.x; m[2, 1] = -f.y; m[2, 2] = -f.z m[0, 3] = -(r.x * camera.position.x + r.y * camera.position.y + r.z * camera.position.z) m[1, 3] = -(u.x * camera.position.x + u.y * camera.position.y + u.z * camera.position.z) m[2, 3] = f.x * camera.position.x + f.y * camera.position.y + f.z * camera.position.z m[3, 3] = 1 return m } // Get the projection matrix for a 3D camera camera_proj_matrix :: proc(camera: Camera3D) -> Mat4 { aspect: f32 = 16.0 / 9.0 if g_state != nil { extent := g_state.backend.get_extent() aspect = f32(extent.width) / f32(extent.height) } near := camera.near if camera.near > 0 else f32(0.1) far := camera.far if camera.far > 0 else f32(100) fovy := camera.fovy if camera.fovy > 0 else f32(60) fovy_rad := fovy * math.PI / 180.0 f := 1.0 / math.tan(fovy_rad * 0.5) m: Mat4 m[0, 0] = f / aspect m[1, 1] = -f // Y-flip for Vulkan NDC m[2, 2] = far / (near - far) m[2, 3] = (near * far) / (near - far) m[3, 2] = -1 return m } // Add trauma to both 2D and 3D camera shake camera_shake :: proc(intensity: f32 = 0.5) { if g_state == nil { return } g_state.shake_2d.trauma = clamp(g_state.shake_2d.trauma + intensity, 0, 1) g_state.shake_3d.trauma = clamp(g_state.shake_3d.trauma + intensity, 0, 1) } // Add trauma to 2D camera shake only camera_shake_2d :: proc(intensity: f32 = 0.5) { if g_state == nil { return } g_state.shake_2d.trauma = clamp(g_state.shake_2d.trauma + intensity, 0, 1) } // Add trauma to 3D camera shake only camera_shake_3d :: proc(intensity: f32 = 0.5) { if g_state == nil { return } g_state.shake_3d.trauma = clamp(g_state.shake_3d.trauma + intensity, 0, 1) } // Configure camera shake parameters set_camera_shake_params :: proc(decay: f32 = 1.0, max_offset: f32 = -1, max_angle: f32 = -1) { if g_state == nil { return } if decay > 0 { g_state.shake_2d.decay = decay g_state.shake_3d.decay = decay } if max_offset >= 0 { g_state.shake_2d.max_offset = max_offset g_state.shake_3d.max_offset = max_offset } if max_angle >= 0 { g_state.shake_2d.max_angle = max_angle } } // Create a 2D camera path from keyframes make_camera_path_2d :: proc(keyframes: []Camera_Keyframe_2D) -> Camera_Path_2D { dur: f32 = 0 if len(keyframes) > 0 { dur = keyframes[len(keyframes) - 1].time } return Camera_Path_2D{keyframes = keyframes, duration = dur} } // Create a 3D camera path from keyframes make_camera_path_3d :: proc(keyframes: []Camera_Keyframe_3D) -> Camera_Path_3D { dur: f32 = 0 if len(keyframes) > 0 { dur = keyframes[len(keyframes) - 1].time } return Camera_Path_3D{keyframes = keyframes, duration = dur} } // Evaluate a 2D camera path at a given time camera_path_eval_2d :: proc(path: Camera_Path_2D, time: f32) -> Camera2D { if len(path.keyframes) == 0 { return default_camera_2d() } if len(path.keyframes) == 1 { return path.keyframes[0].camera } t := clamp(time, 0, path.duration) if t <= path.keyframes[0].time { return path.keyframes[0].camera } if t >= path.keyframes[len(path.keyframes) - 1].time { return path.keyframes[len(path.keyframes) - 1].camera } // Find surrounding keyframes for i := 1; i < len(path.keyframes); i += 1 { if t <= path.keyframes[i].time { prev := path.keyframes[i - 1] next := path.keyframes[i] dt := next.time - prev.time if dt < 1e-8 { return next.camera } local_t := (t - prev.time) / dt return camera_lerp_2d(prev.camera, next.camera, ease(local_t, next.easing)) } } return path.keyframes[len(path.keyframes) - 1].camera } // Evaluate a 3D camera path at a given time camera_path_eval_3d :: proc(path: Camera_Path_3D, time: f32) -> Camera3D { if len(path.keyframes) == 0 { return default_camera_3d() } if len(path.keyframes) == 1 { return path.keyframes[0].camera } t := clamp(time, 0, path.duration) if t <= path.keyframes[0].time { return path.keyframes[0].camera } if t >= path.keyframes[len(path.keyframes) - 1].time { return path.keyframes[len(path.keyframes) - 1].camera } for i := 1; i < len(path.keyframes); i += 1 { if t <= path.keyframes[i].time { prev := path.keyframes[i - 1] next := path.keyframes[i] dt := next.time - prev.time if dt < 1e-8 { return next.camera } local_t := (t - prev.time) / dt return camera_lerp_3d(prev.camera, next.camera, ease(local_t, next.easing)) } } return path.keyframes[len(path.keyframes) - 1].camera } // --- Math Helpers --- matrix_identity :: proc() -> Mat4 { m: Mat4 m[0, 0] = 1 m[1, 1] = 1 m[2, 2] = 1 m[3, 3] = 1 return m } mat4_translate :: proc(v: Vec3) -> Mat4 { // m[row, col] convention m: Mat4 m[0, 0] = 1 m[1, 1] = 1 m[2, 2] = 1 m[3, 3] = 1 m[0, 3] = v.x m[1, 3] = v.y m[2, 3] = v.z return m } mat4_scale :: proc(v: Vec3) -> Mat4 { m: Mat4 m[0, 0] = v.x m[1, 1] = v.y m[2, 2] = v.z m[3, 3] = 1 return m } mat4_rotate_y :: proc(angle_rad: f32) -> Mat4 { c := math.cos(angle_rad) s := math.sin(angle_rad) m: Mat4 m[0, 0] = c m[0, 2] = s m[1, 1] = 1 m[2, 0] = -s m[2, 2] = c m[3, 3] = 1 return m } mat4_rotate_x :: proc(angle_rad: f32) -> Mat4 { c := math.cos(angle_rad) s := math.sin(angle_rad) m: Mat4 m[0, 0] = 1 m[1, 1] = c m[1, 2] = -s m[2, 1] = s m[2, 2] = c m[3, 3] = 1 return m } mat4_rotate_z :: proc(angle_rad: f32) -> Mat4 { c := math.cos(angle_rad) s := math.sin(angle_rad) m: Mat4 m[0, 0] = c m[0, 1] = -s m[1, 0] = s m[1, 1] = c m[2, 2] = 1 m[3, 3] = 1 return m } @(private) vec3_normalize :: proc(v: Vec3) -> Vec3 { l := math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z) if l < 1e-8 { return {} } return v / l } @(private) vec3_cross :: proc(a, b: Vec3) -> Vec3 { return { a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x, } } @(private) vec3_length :: proc(v: Vec3) -> f32 { return math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z) } @(private) shake_noise :: proc(t: f32) -> f32 { // Pseudo-random [-1, 1] from time seed return math.sin(t * 123.456) * math.cos(t * 78.233) } @(private) mat4_mul_vec4 :: proc(m: Mat4, v: Vec4) -> Vec4 { return Vec4{ m[0, 0] * v.x + m[0, 1] * v.y + m[0, 2] * v.z + m[0, 3] * v.w, m[1, 0] * v.x + m[1, 1] * v.y + m[1, 2] * v.z + m[1, 3] * v.w, m[2, 0] * v.x + m[2, 1] * v.y + m[2, 2] * v.z + m[2, 3] * v.w, m[3, 0] * v.x + m[3, 1] * v.y + m[3, 2] * v.z + m[3, 3] * v.w, } } // --- Lighting --- // Enable/disable 3D PBR lighting. When disabled, uses the original unlit shader. set_lighting_enabled :: proc(enabled: bool) { if g_state == nil { return } g_state.render_state.lighting_enabled_3d = enabled } // Enable/disable 2D screen-space lighting. When disabled, uses the original unlit shader. set_lighting_2d_enabled :: proc(enabled: bool) { if g_state == nil { return } g_state.render_state.lighting_enabled_2d = enabled } // Set the ambient light color. Alpha component controls intensity. set_ambient_light :: proc(color: Color = {0.1, 0.1, 0.1, 1.0}) { if g_state == nil { return } g_state.render_state.light_state.ambient_color = color } // Add a light source. Returns light index (0-15), or -1 if full. add_light :: proc(light: Light) -> i32 { if g_state == nil { return -1 } return renderer.add_light(&g_state.render_state.light_state, renderer.Light_Data{ type = u32(light.type), enabled = light.enabled, position = {light.position.x, light.position.y, light.position.z}, color = light.color, intensity = light.intensity, radius = light.radius, casts_shadow = light.casts_shadow, }) } // Update an existing light by index. set_light :: proc(index: i32, light: Light) { if g_state == nil { return } renderer.set_light(&g_state.render_state.light_state, index, renderer.Light_Data{ type = u32(light.type), enabled = light.enabled, position = {light.position.x, light.position.y, light.position.z}, color = light.color, intensity = light.intensity, radius = light.radius, casts_shadow = light.casts_shadow, }) } // Remove a light by index (shifts remaining lights down). remove_light :: proc(index: i32) { if g_state == nil { return } renderer.remove_light(&g_state.render_state.light_state, index) } // Remove all lights. clear_lights :: proc() { if g_state == nil { return } renderer.clear_lights(&g_state.render_state.light_state) } // Get the number of active lights. get_light_count :: proc() -> i32 { if g_state == nil { return 0 } return i32(g_state.render_state.light_state.light_count) } // Get a light by index. get_light :: proc(index: i32) -> Light { if g_state == nil || index < 0 || u32(index) >= g_state.render_state.light_state.light_count { return {} } ld := &g_state.render_state.light_state.lights[index] return Light{ type = Light_Type(ld.type), enabled = ld.enabled, position = {ld.position.x, ld.position.y, ld.position.z}, color = ld.color, intensity = ld.intensity, radius = ld.radius, casts_shadow = ld.casts_shadow, } } // Create a directional light. Direction is the way the light shines (e.g. {0,-1,0} for sun from above). create_directional_light :: proc(direction: Vec3, color: Color = WHITE, intensity: f32 = 1.0) -> Light { return Light{ type = .DIRECTIONAL, enabled = true, position = direction, color = color, intensity = intensity, } } // Create a point light at a world position. create_point_light :: proc(position: Vec3, color: Color = WHITE, intensity: f32 = 1.0, radius: f32 = 10.0) -> Light { return Light{ type = .POINT, enabled = true, position = position, color = color, intensity = intensity, radius = radius, } } // --- Shadows --- // Initialize shadow atlas and depth prepass resources. Call after init. // Returns true on success. Shadow pipelines can be created after this. init_shadows :: proc() -> bool { if g_state == nil { return false } extent := g_state.backend.get_extent() ok := renderer.init_shadow_state( &g_state.render_state.shadow_state, &g_state.backend, extent.width, extent.height, ) return ok } // Query the shadow depth-only render pass for pipeline creation. get_shadow_render_pass :: proc() -> Render_Pass_Handle { if g_state == nil { return {} } return g_state.render_state.shadow_state.render_pass } // Query the shadow descriptor set layout (set 2) for pipeline creation. get_shadow_descriptor_layout :: proc() -> Descriptor_Handle { if g_state == nil { return {} } return g_state.render_state.shadow_state.descriptor_set_layout } // Register an externally-created depth-only pipeline for shadow rendering. set_shadow_depth_pipeline :: proc(variant: Layout_Variant, pipeline: Pipeline_Handle) { if g_state == nil { return } g_state.render_state.shadow_state.depth_pipelines[variant] = pipeline } // Enable/disable shadow mapping for directional lights. set_shadows_enabled :: proc(enabled: bool) { if g_state == nil { return } g_state.render_state.shadows_enabled = enabled g_state.render_state.shadow_state.enabled = enabled } // Set shadow atlas size (default 4096). Call before first frame for best results. set_shadow_resolution :: proc(resolution: u32) { if g_state == nil { return } g_state.render_state.shadow_state.atlas_size = resolution } // Set shadow depth bias to prevent shadow acne. set_shadow_bias :: proc(constant_bias: f32 = 1.25, slope_bias: f32 = 1.75) { if g_state == nil { return } g_state.render_state.shadow_state.bias_constant = constant_bias g_state.render_state.shadow_state.bias_slope = slope_bias } // Set the shadow projection area (half-extent and depth around camera target). set_shadow_area :: proc(half_extent: f32 = 15.0, depth: f32 = 40.0) { if g_state == nil { return } g_state.render_state.shadow_state.shadow_half_extent = half_extent g_state.render_state.shadow_state.shadow_depth = depth } // --- Deferred Lighting (Future) --- // Enable deferred lighting mode (not yet implemented). set_deferred_lighting :: proc(enabled: bool) { log.warn("gpu: deferred lighting is not yet implemented") } // Check if deferred lighting is available. is_deferred_lighting_supported :: proc() -> bool { return false } // Set the G-buffer resolution scale (not yet implemented). set_deferred_resolution_scale :: proc(scale: f32) { log.warn("gpu: deferred lighting is not yet implemented") } @(private) mat4_inverse :: proc(m: Mat4) -> Mat4 { // Cofactor expansion for 4x4 matrix inverse a00 := m[0, 0]; a01 := m[0, 1]; a02 := m[0, 2]; a03 := m[0, 3] a10 := m[1, 0]; a11 := m[1, 1]; a12 := m[1, 2]; a13 := m[1, 3] a20 := m[2, 0]; a21 := m[2, 1]; a22 := m[2, 2]; a23 := m[2, 3] a30 := m[3, 0]; a31 := m[3, 1]; a32 := m[3, 2]; a33 := m[3, 3] b00 := a00 * a11 - a01 * a10 b01 := a00 * a12 - a02 * a10 b02 := a00 * a13 - a03 * a10 b03 := a01 * a12 - a02 * a11 b04 := a01 * a13 - a03 * a11 b05 := a02 * a13 - a03 * a12 b06 := a20 * a31 - a21 * a30 b07 := a20 * a32 - a22 * a30 b08 := a20 * a33 - a23 * a30 b09 := a21 * a32 - a22 * a31 b10 := a21 * a33 - a23 * a31 b11 := a22 * a33 - a23 * a32 det := b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06 if math.abs(det) < 1e-12 { return matrix_identity() } inv_det := 1.0 / det r: Mat4 r[0, 0] = ( a11 * b11 - a12 * b10 + a13 * b09) * inv_det r[0, 1] = (-a01 * b11 + a02 * b10 - a03 * b09) * inv_det r[0, 2] = ( a31 * b05 - a32 * b04 + a33 * b03) * inv_det r[0, 3] = (-a21 * b05 + a22 * b04 - a23 * b03) * inv_det r[1, 0] = (-a10 * b11 + a12 * b08 - a13 * b07) * inv_det r[1, 1] = ( a00 * b11 - a02 * b08 + a03 * b07) * inv_det r[1, 2] = (-a30 * b05 + a32 * b02 - a33 * b01) * inv_det r[1, 3] = ( a20 * b05 - a22 * b02 + a23 * b01) * inv_det r[2, 0] = ( a10 * b10 - a11 * b08 + a13 * b06) * inv_det r[2, 1] = (-a00 * b10 + a01 * b08 - a03 * b06) * inv_det r[2, 2] = ( a30 * b04 - a31 * b02 + a33 * b00) * inv_det r[2, 3] = (-a20 * b04 + a21 * b02 - a23 * b00) * inv_det r[3, 0] = (-a10 * b09 + a11 * b07 - a12 * b06) * inv_det r[3, 1] = ( a00 * b09 - a01 * b07 + a02 * b06) * inv_det r[3, 2] = (-a30 * b03 + a31 * b01 - a32 * b00) * inv_det r[3, 3] = ( a20 * b03 - a21 * b01 + a22 * b00) * inv_det return r }