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, 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 { if !sdl.Init({.VIDEO}) { fmt.eprintfln("SDL_Init failed: %s", sdl.GetError()) return false } 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 } return true } shutdown :: proc(app: ^App) { 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^ = {} } events :: proc() -> bool { event: sdl.Event for sdl.PollEvent(&event) { if event.type == .QUIT { return false } } return true } 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) { 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 } 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 }