From ebb6ea383f7030390253c5fa83afb67039456b54 Mon Sep 17 00:00:00 2001 From: codegirl-007 Date: Fri, 31 Jul 2026 19:11:19 -0700 Subject: [PATCH] Initial pipeline work --- engine/app.odin | 253 +++++++++++++++++++++++++++++++++++-- engine/character_data.odin | 125 ++++++++++++++++-- engine/sprite.odin | 110 +++++++++++++--- examples/toad/main.odin | 20 +-- shaders/sprite.frag.glsl | 10 ++ shaders/sprite.frag.spv | Bin 0 -> 556 bytes shaders/sprite.vert.glsl | 11 ++ shaders/sprite.vert.spv | Bin 0 -> 968 bytes 8 files changed, 475 insertions(+), 54 deletions(-) create mode 100644 shaders/sprite.frag.glsl create mode 100644 shaders/sprite.frag.spv create mode 100644 shaders/sprite.vert.glsl create mode 100644 shaders/sprite.vert.spv diff --git a/engine/app.odin b/engine/app.odin index b4e8736..d927825 100644 --- a/engine/app.odin +++ b/engine/app.odin @@ -3,9 +3,29 @@ package engine import "core:fmt" import sdl "vendor:sdl3" +Vertex :: struct { + pos: [2]f32, // clip space, -1..1 + uv: [2]f32, // 0..1 atlas coords (used later in draw_sprite) +} + +SPRITE_VERT_COUNT :: 6 +VERTEX_BUFFER_SIZE :: SPRITE_VERT_COUNT * size_of(Vertex) + +VERT_SPV := #load("../shaders/sprite.vert.spv") +FRAG_SPV := #load("../shaders/sprite.frag.spv") + App :: struct { - window: ^sdl.Window, - renderer: ^sdl.Renderer, + window: ^sdl.Window, + device: ^sdl.GPUDevice, + pipeline: ^sdl.GPUGraphicsPipeline, + sampler: ^sdl.GPUSampler, + cmd: ^sdl.GPUCommandBuffer, + render_pass: ^sdl.GPURenderPass, + swapchain_texture: ^sdl.GPUTexture, + swapchain_w: u32, + swapchain_h: u32, + vertex_buffer: ^sdl.GPUBuffer, + transfer_buffer: ^sdl.GPUTransferBuffer, } init :: proc(app: ^App, title: cstring, width, height: i32) -> bool { @@ -14,8 +34,64 @@ init :: proc(app: ^App, title: cstring, width, height: i32) -> bool { return false } - if !sdl.CreateWindowAndRenderer(title, width, height, {}, &app.window, &app.renderer) { - fmt.eprintfln("CreateWindowAndRenderer failed: %s", sdl.GetError()) + app.window = sdl.CreateWindow(title, width, height, {}) + + if app.window == nil { + fmt.eprintfln("CreateWindow failed: %s", sdl.GetError()) + return false + } + + app.device = sdl.CreateGPUDevice({.SPIRV}, true, nil) + if app.device == nil { + fmt.eprintfln("CreateGPUDevice failed: %s", sdl.GetError()) + return false + } + + if !sdl.ClaimWindowForGPUDevice(app.device, app.window) { + fmt.eprintfln("ClaimWindowForGPUDevice failed: %s", sdl.GetError()) + return false + } + + app.pipeline = create_sprite_pipeline(app.device, app.window) + if app.pipeline == nil { + fmt.eprintfln("create_sprite_pipeline failed: %s", sdl.GetError()) + return false + } + + app.sampler = sdl.CreateGPUSampler( + app.device, + { + min_filter = .NEAREST, + mag_filter = .NEAREST, + mipmap_mode = .NEAREST, + address_mode_u = .CLAMP_TO_EDGE, + address_mode_v = .CLAMP_TO_EDGE, + address_mode_w = .CLAMP_TO_EDGE, + }, + ) + + if app.sampler == nil { + fmt.eprintfln("CreateGPUSampler failed: %s", sdl.GetError()) + return false + } + + app.vertex_buffer = sdl.CreateGPUBuffer( + app.device, + {usage = {.VERTEX}, size = VERTEX_BUFFER_SIZE}, + ) + + if app.vertex_buffer == nil { + fmt.eprintfln("CreateGPUBuffer failed: %s", sdl.GetError()) + return false + } + + app.transfer_buffer = sdl.CreateGPUTransferBuffer( + app.device, + {usage = .UPLOAD, size = VERTEX_BUFFER_SIZE}, + ) + + if app.transfer_buffer == nil { + fmt.eprintfln("CreateGPUTransferBuffer failed: %s", sdl.GetError()) return false } @@ -23,14 +99,43 @@ init :: proc(app: ^App, title: cstring, width, height: i32) -> bool { } shutdown :: proc(app: ^App) { - if app.renderer != nil do sdl.DestroyRenderer(app.renderer) - if app.window != nil do sdl.DestroyWindow(app.window) + if app.device != nil { + ok := sdl.WaitForGPUIdle(app.device) + if !ok { + fmt.eprintfln("WaitForGPUIdle failed") + } + + if app.transfer_buffer != nil { + sdl.ReleaseGPUTransferBuffer(app.device, app.transfer_buffer) + } + + if app.vertex_buffer != nil { + sdl.ReleaseGPUBuffer(app.device, app.vertex_buffer) + } + + if app.sampler != nil { + sdl.ReleaseGPUSampler(app.device, app.sampler) + } + + if app.pipeline != nil { + sdl.ReleaseGPUGraphicsPipeline(app.device, app.pipeline) + } + + if app.window != nil { + sdl.ReleaseWindowFromGPUDevice(app.device, app.window) + } + sdl.DestroyGPUDevice(app.device) + } + + if app.window != nil { + sdl.DestroyWindow(app.window) + } sdl.Quit() app^ = {} } -pump_events :: proc() -> bool { +events :: proc() -> bool { event: sdl.Event for sdl.PollEvent(&event) { @@ -42,15 +147,137 @@ pump_events :: proc() -> bool { return true } -begin_frame :: proc(app: ^App, r: u8 = 30, g: u8 = 30, b: u8 = 40, a: u8 = 255) { - sdl.SetRenderDrawColor(app.renderer, r, g, b, a) - sdl.RenderClear(app.renderer) +begin_frame :: proc(app: ^App, clear: sdl.FColor = {0.12, 0.12, 0.16, 1}) { + app.cmd = sdl.AcquireGPUCommandBuffer(app.device) + + if app.cmd == nil { + fmt.eprintfln("AcquireGPUCommandBuffer failed: %s", sdl.GetError()) + return + } + + ok := sdl.WaitAndAcquireGPUSwapchainTexture( + app.cmd, + app.window, + &app.swapchain_texture, + &app.swapchain_w, + &app.swapchain_h, + ) + + if !ok || app.swapchain_texture == nil { + return + } + + color_info := sdl.GPUColorTargetInfo { + texture = app.swapchain_texture, + clear_color = clear, + load_op = .CLEAR, + store_op = .STORE, + } + + app.render_pass = sdl.BeginGPURenderPass(app.cmd, &color_info, 1, nil) + + sdl.BindGPUGraphicsPipeline(app.render_pass, app.pipeline) } end_frame :: proc(app: ^App) { - sdl.RenderPresent(app.renderer) + if app.render_pass != nil { + sdl.EndGPURenderPass(app.render_pass) + app.render_pass = nil + } + + if app.cmd != nil { + ok := sdl.SubmitGPUCommandBuffer(app.cmd) + if !ok { + fmt.eprintfln("SubmitGPUCommandBuffer failed") + } + app.cmd = nil + } + + app.swapchain_texture = nil } -renderer :: proc(app: ^App) -> ^sdl.Renderer { - return app.renderer +load_spirv_shader :: proc( + device: ^sdl.GPUDevice, + code: []u8, + stage: sdl.GPUShaderStage, + num_samplers: u32, +) -> ^sdl.GPUShader { + return sdl.CreateGPUShader( + device, + { + code_size = len(code), + code = raw_data(code), + entrypoint = "main", + format = {.SPIRV}, + stage = stage, + num_samplers = num_samplers, + num_storage_textures = 0, + num_storage_buffers = 0, + num_uniform_buffers = 0, + }, + ) +} + +create_sprite_pipeline :: proc( + device: ^sdl.GPUDevice, + window: ^sdl.Window, +) -> ^sdl.GPUGraphicsPipeline { + vert := load_spirv_shader(device, VERT_SPV[:], .VERTEX, 0) + if vert == nil do return nil + + frag := load_spirv_shader(device, FRAG_SPV[:], .FRAGMENT, 1) + if frag == nil { + sdl.ReleaseGPUShader(device, vert) + return nil + } + + swap_format := sdl.GetGPUSwapchainTextureFormat(device, window) + + + blend := sdl.GPUColorTargetBlendState { + src_color_blendfactor = .SRC_ALPHA, + dst_color_blendfactor = .ONE_MINUS_SRC_ALPHA, + color_blend_op = .ADD, + src_alpha_blendfactor = .ONE, + dst_alpha_blendfactor = .ONE_MINUS_SRC_ALPHA, + alpha_blend_op = .ADD, + enable_blend = true, + } + + color_target := sdl.GPUColorTargetDescription { + format = swap_format, + blend_state = blend, + } + + vb_desc := sdl.GPUVertexBufferDescription { + slot = 0, + pitch = u32(size_of(Vertex)), + input_rate = .VERTEX, + } + + attrs := [2]sdl.GPUVertexAttribute { + {location = 0, buffer_slot = 0, format = .FLOAT2, offset = 0}, + {location = 1, buffer_slot = 0, format = .FLOAT2, offset = u32(offset_of(Vertex, uv))}, + } + + pipeline_info := sdl.GPUGraphicsPipelineCreateInfo { + vertex_shader = vert, + fragment_shader = frag, + vertex_input_state = { + vertex_buffer_descriptions = &vb_desc, + num_vertex_buffers = 1, + vertex_attributes = raw_data(attrs[:]), + num_vertex_attributes = 2, + }, + primitive_type = .TRIANGLELIST, + rasterizer_state = {fill_mode = .FILL}, + target_info = {color_target_descriptions = &color_target, num_color_targets = 1}, + } + + pipeline := sdl.CreateGPUGraphicsPipeline(device, pipeline_info) + + sdl.ReleaseGPUShader(device, vert) + sdl.ReleaseGPUShader(device, frag) + + return pipeline } diff --git a/engine/character_data.odin b/engine/character_data.odin index 4fc6d68..b378bd9 100644 --- a/engine/character_data.odin +++ b/engine/character_data.odin @@ -2,6 +2,7 @@ package engine import "core:encoding/json" import "core:fmt" +import "core:mem" import "core:os" import "core:path/filepath" import "core:strings" @@ -30,16 +31,12 @@ Char_Def :: struct { Character_Data :: struct { def: Char_Def, - texture: ^sdl.Texture, + texture: ^sdl.GPUTexture, + width: int, + height: int, } -load_character_data :: proc( - renderer: ^sdl.Renderer, - character_json_path: string, -) -> ( - Character_Data, - bool, -) { +load_character_data :: proc(app: ^App, character_json_path: string) -> (Character_Data, bool) { file_data, read_err := os.read_entire_file(character_json_path, context.allocator) if read_err != nil { fmt.eprintfln("failed to read %s: %v", character_json_path, read_err) @@ -75,18 +72,120 @@ load_character_data :: proc( } defer sdl.DestroySurface(surface) - out.texture = sdl.CreateTextureFromSurface(renderer, surface) + rgba_surface := surface + converted_surface: ^sdl.Surface + defer { + if converted_surface != nil do sdl.DestroySurface(converted_surface) + } + if surface.format != .ABGR8888 { + converted_surface = sdl.ConvertSurface(surface, .ABGR8888) + if converted_surface == nil { + fmt.eprintfln("ConvertSurface(ABGR8888) failed: %s", sdl.GetError()) + return {}, false + } + rgba_surface = converted_surface + } + + out.width = int(rgba_surface.w) + out.height = int(rgba_surface.h) + + out.texture = sdl.CreateGPUTexture( + app.device, + { + type = .D2, + format = .R8G8B8A8_UNORM, + usage = {.SAMPLER}, + width = u32(out.width), + height = u32(out.height), + layer_count_or_depth = 1, + num_levels = 1, + sample_count = ._1, + }, + ) + if out.texture == nil { - fmt.eprintfln("CreateTextureFromSurface failed: %s", sdl.GetError()) + fmt.eprintfln("CreateGPUTexture failed: %s", sdl.GetError()) return {}, false } - sdl.SetTextureScaleMode(out.texture, .NEAREST) + + upload_size := int(rgba_surface.pitch) * int(rgba_surface.h) + tbuf := sdl.CreateGPUTransferBuffer(app.device, {usage = .UPLOAD, size = u32(upload_size)}) + + if tbuf == nil { + fmt.eprintfln("CreateGPUTransferBuffer failed: %s", sdl.GetError()) + sdl.ReleaseGPUTexture(app.device, out.texture) + return {}, false + } + + map_ptr := sdl.MapGPUTransferBuffer(app.device, tbuf, false) + + if map_ptr == nil { + fmt.eprintfln("MapGPUTransferBuffer failed: %s", sdl.GetError()) + sdl.ReleaseGPUTransferBuffer(app.device, tbuf) + sdl.ReleaseGPUTexture(app.device, out.texture) + return {}, false + } + + mem.copy(map_ptr, rgba_surface.pixels, upload_size) + sdl.UnmapGPUTransferBuffer(app.device, tbuf) + + cmd := sdl.AcquireGPUCommandBuffer(app.device) + if cmd == nil { + fmt.eprintfln("AcquireGPUCommandBuffer failed: %s", sdl.GetError()) + sdl.ReleaseGPUTransferBuffer(app.device, tbuf) + sdl.ReleaseGPUTexture(app.device, out.texture) + return {}, false + } + + copy_pass := sdl.BeginGPUCopyPass(cmd) + + transfer := sdl.GPUTextureTransferInfo { + transfer_buffer = tbuf, + offset = 0, + pixels_per_row = u32(rgba_surface.pitch / 4), // RGBA8888 => 4 bytes/pixel + rows_per_layer = u32(rgba_surface.h), + } + region := sdl.GPUTextureRegion { + texture = out.texture, + mip_level = 0, + layer = 0, + x = 0, + y = 0, + z = 0, + w = u32(rgba_surface.w), + h = u32(rgba_surface.h), + d = 1, + } + sdl.UploadToGPUTexture(copy_pass, transfer, region, false) + sdl.EndGPUCopyPass(copy_pass) + fence := sdl.SubmitGPUCommandBufferAndAcquireFence(cmd) + if fence == nil { + fmt.eprintfln("SubmitGPUCommandBufferAndAcquireFence (texture upload) failed: %s", sdl.GetError()) + sdl.ReleaseGPUTransferBuffer(app.device, tbuf) + sdl.ReleaseGPUTexture(app.device, out.texture) + return {}, false + } + defer sdl.ReleaseGPUFence(app.device, fence) + + if !sdl.WaitForGPUFences(app.device, true, &fence, 1) { + fmt.eprintfln("WaitForGPUFences (texture upload) failed: %s", sdl.GetError()) + sdl.ReleaseGPUTransferBuffer(app.device, tbuf) + sdl.ReleaseGPUTexture(app.device, out.texture) + return {}, false + } + + sdl.ReleaseGPUTransferBuffer(app.device, tbuf) + return out, true } -destroy_character_data :: proc(data: ^Character_Data) { +destroy_character_data :: proc(app: ^App, data: ^Character_Data) { if data.texture != nil { - sdl.DestroyTexture(data.texture) + // Ensure no in-flight draw command is still sampling this texture. + if app != nil && app.device != nil { + _ = sdl.WaitForGPUIdle(app.device) + } + sdl.ReleaseGPUTexture(app.device, data.texture) } data^ = {} } diff --git a/engine/sprite.odin b/engine/sprite.odin index 602f3cf..22c11f2 100644 --- a/engine/sprite.odin +++ b/engine/sprite.odin @@ -1,5 +1,6 @@ package engine +import "core:mem" import sdl "vendor:sdl3" Vec2 :: [2]f32 @@ -26,26 +27,105 @@ update_sprite :: proc(sprite: ^Sprite, dt: f32) { _ = dt } -draw_sprite :: proc(renderer: ^sdl.Renderer, sprite: ^Sprite) { - assert(sprite.data != nil) +to_clip :: proc(px, py, sw, sh: f32) -> [2]f32 { + return { + px / sw * 2 - 1, + 1 - py / sh * 2, // flip y (window pixels are y-down) + } +} + +draw_sprite :: proc(app: ^App, sprite: ^Sprite) { + if app.render_pass == nil || app.cmd == nil || app.swapchain_texture == nil { + return // begin_frame may have skipped (minimized window, etc.) + } + if sprite == nil || sprite.data == nil || sprite.data.texture == nil { + return + } + rect, ok := character_frame_rect(sprite.data, sprite.clip, sprite.frame) if !ok { return } - w := f32(rect[2]) - h := f32(rect[3]) - dst := sdl.FRect { - x = sprite.position.x - w * 0.5, - y = sprite.position.y - h, - w = w, - h = h, + fw := f32(rect[2]) + fh := f32(rect[3]) + + // Feet-centered placement, same as renderer version + x0_px := sprite.position.x - fw * 0.5 + y0_px := sprite.position.y - fh + x1_px := x0_px + fw + y1_px := y0_px + fh + + sw := f32(app.swapchain_w) + sh := f32(app.swapchain_h) + p0 := to_clip(x0_px, y0_px, sw, sh) // top-left + p1 := to_clip(x1_px, y0_px, sw, sh) // top-right + p2 := to_clip(x1_px, y1_px, sw, sh) // bottom-right + p3 := to_clip(x0_px, y1_px, sw, sh) // bottom-left + + tex_w := f32(sprite.data.width) + tex_h := f32(sprite.data.height) + u0 := f32(rect[0]) / tex_w + v0 := f32(rect[1]) / tex_h + u1 := f32(rect[0] + rect[2]) / tex_w + v1 := f32(rect[1] + rect[3]) / tex_h + + // Two triangles: (0,1,2) and (0,2,3) + verts := [6]Vertex { + {pos = p0, uv = {u0, v0}}, + {pos = p1, uv = {u1, v0}}, + {pos = p2, uv = {u1, v1}}, + {pos = p0, uv = {u0, v0}}, + {pos = p2, uv = {u1, v1}}, + {pos = p3, uv = {u0, v1}}, } - src := sdl.FRect { - x = f32(rect[0]), - y = f32(rect[1]), - w = w, - h = h, + + map_ptr := sdl.MapGPUTransferBuffer(app.device, app.transfer_buffer, false) + if map_ptr == nil { + return } - sdl.RenderTexture(renderer, sprite.data.texture, &src, &dst) + mem.copy(map_ptr, raw_data(verts[:]), size_of(verts)) + sdl.UnmapGPUTransferBuffer(app.device, app.transfer_buffer) + + // IMPORTANT: SDL does not allow beginning a copy pass while a render pass + // is active on the same command buffer. Upload on a separate command buffer. + copy_cmd := sdl.AcquireGPUCommandBuffer(app.device) + if copy_cmd == nil { + return + } + copy_pass := sdl.BeginGPUCopyPass(copy_cmd) + + src := sdl.GPUTransferBufferLocation { + transfer_buffer = app.transfer_buffer, + offset = 0, + } + dst := sdl.GPUBufferRegion { + buffer = app.vertex_buffer, + offset = 0, + size = u32(size_of(verts)), + } + sdl.UploadToGPUBuffer(copy_pass, src, dst, false) + sdl.EndGPUCopyPass(copy_pass) + fence := sdl.SubmitGPUCommandBufferAndAcquireFence(copy_cmd) + if fence == nil { + return + } + defer sdl.ReleaseGPUFence(app.device, fence) + if !sdl.WaitForGPUFences(app.device, true, &fence, 1) { + return + } + + sampler_binding := sdl.GPUTextureSamplerBinding { + texture = sprite.data.texture, + sampler = app.sampler, + } + sdl.BindGPUFragmentSamplers(app.render_pass, 0, &sampler_binding, 1) + + vb_binding := sdl.GPUBufferBinding { + buffer = app.vertex_buffer, + offset = 0, + } + sdl.BindGPUVertexBuffers(app.render_pass, 0, &vb_binding, 1) + + sdl.DrawGPUPrimitives(app.render_pass, 6, 1, 0, 0) } diff --git a/examples/toad/main.odin b/examples/toad/main.odin index f4e6ce6..ad21f35 100644 --- a/examples/toad/main.odin +++ b/examples/toad/main.odin @@ -4,25 +4,19 @@ import eng "pkg:engine" main :: proc() { app: eng.App - if !eng.init(&app, "toad example", 800, 600) { - return - } + if !eng.init(&app, "toad", 800, 600) do return defer eng.shutdown(&app) - data, ok := eng.load_character_data( - app.renderer, - "assets_baked/characters/toad/toad.char.json", - ) - if !ok { - return - } - defer eng.destroy_character_data(&data) + data, ok := eng.load_character_data(&app, "assets_baked/characters/toad/toad.char.json") + if !ok do return + + defer eng.destroy_character_data(&app, &data) toad := eng.spawn_sprite(&data, {400, 500}, "idle", 0) - for eng.pump_events() { + for eng.events() { eng.begin_frame(&app) - eng.draw_sprite(app.renderer, &toad) + eng.draw_sprite(&app, &toad) eng.end_frame(&app) } } diff --git a/shaders/sprite.frag.glsl b/shaders/sprite.frag.glsl new file mode 100644 index 0000000..3a57860 --- /dev/null +++ b/shaders/sprite.frag.glsl @@ -0,0 +1,10 @@ +#version 450 + +layout(set = 2, binding = 0) uniform sampler2D u_tex; // SDL_GPU fragment sampler slot 0 + +layout(location = 0) in vec2 v_uv; +layout(location = 0) out vec4 out_color; + +void main() { + out_color = texture(u_tex, v_uv); +} diff --git a/shaders/sprite.frag.spv b/shaders/sprite.frag.spv new file mode 100644 index 0000000000000000000000000000000000000000..9771d25d02adac65a73c82216a9aaa694f3de1ac GIT binary patch literal 556 zcmY+A%Syvw5QWFawAE@aRumDW)qUwgMG)O~)13>KLDYqy1~e%?pHJmR@cYsfbzn08 zIdkTgq;b-USc^`yV>gyIhz3l+HLEzA-^?fP#lz(EY^tIgNhMU%i&m@?^_RP+R}&lf z0PmxdX8^B+t`VOUrLceXDU3CFS!DP5bNEr60n|kE_j=nmyi4xDmdO-o3@{I^x?Vdb#{%jom@+fNeI3>N)YBt?(hr z4jFa!+p1M^_7i)9=KLPfe(tHhPqdf3da*ajo%6@kxvS<*Uoa{03f382-2P&F?BLel UqVlF8u15Zv4&EmB&4dg52QVrbr2qf` literal 0 HcmV?d00001 diff --git a/shaders/sprite.vert.glsl b/shaders/sprite.vert.glsl new file mode 100644 index 0000000..0de19f5 --- /dev/null +++ b/shaders/sprite.vert.glsl @@ -0,0 +1,11 @@ +#version 450 + +layout(location = 0) in vec2 in_pos; +layout(location = 1) in vec2 in_uv; + +layout(location = 0) out vec2 v_uv; + +void main() { + v_uv = in_uv; + gl_Position = vec4(in_pos, 0.0, 1.0); +} diff --git a/shaders/sprite.vert.spv b/shaders/sprite.vert.spv new file mode 100644 index 0000000000000000000000000000000000000000..6b0906aa354317a1eb80d2c2e26ab5a281f8caca GIT binary patch literal 968 zcmYL`-Ae*N6vZdM%hJr$KFl_H45WvOAS$B39(*u_UPWq&g>4mGC3@;_>#yo1=={bV zyUX2s@408@o|#Ffj`PN>n4&3|L$k1oNeMB+ayE9l{cd|2zqZfLPhpfy+7jWE&8p2a z`o9l@hytW2ugdH4w7!($w-#hgM&Cb|8vcARn=9BpYlCP3WTl~^`Yjyv{K>OFiTw|F zxdbPtH<|`00veNnq!Lpigbw1PQ%axlzc`^v%4 h*VB&7ZOY-&Lo533vG<| literal 0 HcmV?d00001