Files
codegirl-games/engine/app.odin
T
2026-07-31 19:11:19 -07:00

284 lines
6.3 KiB
Odin

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
}