Files

582 lines
13 KiB
Odin
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package engine
import "core:fmt"
import "core:mem"
import "core:path/filepath"
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
MAX_SPRITES :: 128
SPRITE_VERTS_SIZE :: SPRITE_VERT_COUNT * size_of(Vertex)
VERTEX_BUFFER_SIZE :: MAX_SPRITES * SPRITE_VERTS_SIZE
Queued_Sprite :: struct {
texture: ^sdl.GPUTexture,
verts: [SPRITE_VERT_COUNT]Vertex,
batch_group: u32, // 0 = strict order; nonzero = caller permits regrouping
}
Draw_Batch :: struct {
start: int,
count: int,
texture: ^sdl.GPUTexture,
}
App :: struct {
window: ^sdl.Window,
device: ^sdl.GPUDevice,
shader: Shader_Runtime,
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,
draw_list: [dynamic]Queued_Sprite,
clear_color: sdl.FColor,
camera: Camera,
}
Shader_Backend :: enum {
Vulkan_SPIRV,
DSD12_DXIL,
Metal_MSL,
}
Shader_Runtime :: struct {
backend: Shader_Backend,
format: sdl.GPUShaderFormat,
shader_dir: string,
entrypoint: cstring,
}
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
}
requested: sdl.GPUShaderFormat = {.SPIRV, .DXIL, .MSL}
app.device = sdl.CreateGPUDevice(requested, false, 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
}
if !sdl.SetGPUAllowedFramesInFlight(app.device, 3) {
fmt.eprintfln("SetGPUAllowedFramesInFlight failed: %s", sdl.GetError())
}
// Prefer uncapped present for profiling; fall back if unsupported.
present := sdl.GPUPresentMode.VSYNC
if sdl.WindowSupportsGPUPresentMode(app.device, app.window, .IMMEDIATE) {
present = .IMMEDIATE
} else if sdl.WindowSupportsGPUPresentMode(app.device, app.window, .MAILBOX) {
present = .MAILBOX
}
if present != .VSYNC {
if !sdl.SetGPUSwapchainParameters(app.device, app.window, .SDR, present) {
fmt.eprintfln("SetGPUSwapchainParameters failed: %s", sdl.GetError())
}
}
ok: bool
app.shader, ok = choose_shader_runtime(app.device)
if !ok do return false
app.pipeline = create_sprite_pipeline(app)
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
}
app.draw_list = make([dynamic]Queued_Sprite)
app.camera = camera_default()
return true
}
shutdown :: proc(app: ^App) {
delete(app.draw_list)
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_color: sdl.FColor = {0.12, 0.12, 0.16, 1}) {
clear(&app.draw_list)
app.clear_color = clear_color
app.render_pass = nil
app.swapchain_texture = nil
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 {
app.swapchain_texture = nil
return
}
}
end_frame :: proc(app: ^App) {
if app.cmd == nil {
clear(&app.draw_list)
return
}
cmd := app.cmd
defer {
app.render_pass = nil
app.swapchain_texture = nil
app.cmd = nil
clear(&app.draw_list)
}
if app.swapchain_texture == nil {
if !sdl.SubmitGPUCommandBuffer(cmd) {
fmt.eprintfln("SubmitGPUCommandBuffer failed")
}
return
}
n := len(app.draw_list)
batches: [dynamic]Draw_Batch
defer delete(batches)
if n > 0 {
prepare_draw_batches(app.draw_list[:], &batches)
map_ptr := sdl.MapGPUTransferBuffer(app.device, app.transfer_buffer, false)
if map_ptr == nil {
fmt.eprintfln("MapGPUTransferBuffer failed: %s", sdl.GetError())
if !sdl.SubmitGPUCommandBuffer(cmd) {
fmt.eprintfln("SubmitGPUCommandBuffer failed")
}
return
}
for i in 0 ..< n {
q := &app.draw_list[i]
offset := i * SPRITE_VERTS_SIZE
mem.copy(
rawptr(uintptr(map_ptr) + uintptr(offset)),
raw_data(q.verts[:]),
SPRITE_VERTS_SIZE,
)
}
sdl.UnmapGPUTransferBuffer(app.device, app.transfer_buffer)
copy_pass := sdl.BeginGPUCopyPass(cmd)
src := sdl.GPUTransferBufferLocation {
transfer_buffer = app.transfer_buffer,
offset = 0,
}
dst := sdl.GPUBufferRegion {
buffer = app.vertex_buffer,
offset = 0,
size = u32(n * SPRITE_VERTS_SIZE),
}
sdl.UploadToGPUBuffer(copy_pass, src, dst, false)
sdl.EndGPUCopyPass(copy_pass)
}
color_info := sdl.GPUColorTargetInfo {
texture = app.swapchain_texture,
clear_color = app.clear_color,
load_op = .CLEAR,
store_op = .STORE,
}
app.render_pass = sdl.BeginGPURenderPass(cmd, &color_info, 1, nil)
sdl.BindGPUGraphicsPipeline(app.render_pass, app.pipeline)
for batch in batches {
sampler_binding := sdl.GPUTextureSamplerBinding {
texture = batch.texture,
sampler = app.sampler,
}
sdl.BindGPUFragmentSamplers(app.render_pass, 0, &sampler_binding, 1)
vb_binding := sdl.GPUBufferBinding {
buffer = app.vertex_buffer,
offset = u32(batch.start * SPRITE_VERTS_SIZE),
}
sdl.BindGPUVertexBuffers(app.render_pass, 0, &vb_binding, 1)
sdl.DrawGPUPrimitives(app.render_pass, u32(batch.count * SPRITE_VERT_COUNT), 1, 0, 0)
}
sdl.EndGPURenderPass(app.render_pass)
app.render_pass = nil
if !sdl.SubmitGPUCommandBuffer(cmd) {
fmt.eprintfln("SubmitGPUCommandBuffer failed")
}
}
load_gpu_shader :: proc(
device: ^sdl.GPUDevice,
code: []u8,
stage: sdl.GPUShaderStage,
format: sdl.GPUShaderFormat,
entrypoint: cstring,
num_samplers: u32,
) -> ^sdl.GPUShader {
return sdl.CreateGPUShader(
device,
{
code_size = len(code),
code = raw_data(code),
entrypoint = entrypoint,
format = format,
stage = stage,
num_samplers = num_samplers,
num_storage_textures = 0,
num_storage_buffers = 0,
num_uniform_buffers = 0,
},
)
}
shader_filenames :: proc(backend: Shader_Backend) -> (vert_name, frag_name: string) {
switch backend {
case .Vulkan_SPIRV:
return "sprite.vert.spv", "sprite.frag.spv"
case .DSD12_DXIL:
return "sprite.vert.dxil", "sprite.frag.dxil"
case .Metal_MSL:
return "sprite.vert.msl", "sprite.frag.msl"
}
return "", ""
}
create_sprite_pipeline :: proc(app: ^App) -> ^sdl.GPUGraphicsPipeline {
vert_name, frag_name := shader_filenames(app.shader.backend)
vert_path, _ := filepath.join({app.shader.shader_dir, vert_name})
frag_path, _ := filepath.join({app.shader.shader_dir, frag_name})
defer {
delete(vert_path)
delete(frag_path)
}
vert_code, vok := load_shader_blob(vert_path)
if !vok do return nil
defer delete(vert_code)
frag_code, fok := load_shader_blob(frag_path)
if !fok do return nil
defer delete(frag_code)
vert := load_gpu_shader(
app.device,
vert_code,
.VERTEX,
app.shader.format,
app.shader.entrypoint,
0,
)
if vert == nil do return nil
frag := load_gpu_shader(
app.device,
frag_code,
.FRAGMENT,
app.shader.format,
app.shader.entrypoint,
1,
)
if frag == nil {
sdl.ReleaseGPUShader(app.device, vert)
return nil
}
swap_format := sdl.GetGPUSwapchainTextureFormat(app.device, app.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(app.device, pipeline_info)
sdl.ReleaseGPUShader(app.device, vert)
sdl.ReleaseGPUShader(app.device, frag)
return pipeline
}
texture_run_len :: proc(list: []Queued_Sprite, start: int) -> int {
if start < 0 || start >= len(list) do return 0
tex := list[start].texture
n := 1
for i in start + 1 ..< len(list) {
if list[i].texture != tex do break
n += 1
}
return n
}
texture_run_count :: proc(list: []Queued_Sprite) -> int {
if len(list) == 0 do return 0
count := 0
i := 0
for i < len(list) {
run := texture_run_len(list, i)
count += 1
i += run
}
return count
}
prepare_draw_batches :: proc(list: []Queued_Sprite, batches: ^[dynamic]Draw_Batch) {
clear(batches)
group_texture_runs(list)
i := 0
for i < len(list) {
run := texture_run_len(list, i)
append(batches, Draw_Batch{start = i, count = run, texture = list[i].texture})
i += run
}
}
// Within each contiguous nonzero batch_group, stably sort by texture so
// consecutive same-texture sprites become one draw. Group 0 and group
// boundaries are never crossed.
group_texture_runs :: proc(list: []Queued_Sprite) {
start := 0
for start < len(list) {
group := list[start].batch_group
if group == 0 {
start += 1
continue
}
end := start + 1
for end < len(list) && list[end].batch_group == group {
end += 1
}
stable_group_by_texture(list[start:end])
start = end
}
}
// Stably orders window by texture pointer. Fast path for 12 textures
// (common); insertion sort for 3+.
stable_group_by_texture :: proc(window: []Queued_Sprite) {
n := len(window)
if n <= 1 do return
already := true
for i in 1 ..< n {
if uintptr(window[i].texture) < uintptr(window[i - 1].texture) {
already = false
break
}
}
if already do return
first := uintptr(window[0].texture)
second: uintptr
has_second := false
third := false
for i in 1 ..< n {
t := uintptr(window[i].texture)
if t == first do continue
if !has_second {
second = t
has_second = true
continue
}
if t != second {
third = true
break
}
}
if !has_second do return
if third {
for i in 1 ..< n {
item := window[i]
j := i
for j > 0 {
if uintptr(window[j - 1].texture) <= uintptr(item.texture) {
break
}
window[j] = window[j - 1]
j -= 1
}
window[j] = item
}
return
}
lo, hi := first, second
if lo > hi do lo, hi = hi, lo
tmp: [MAX_SPRITES]Queued_Sprite
w := 0
for q in window {
if uintptr(q.texture) == lo {
tmp[w] = q
w += 1
}
}
for q in window {
if uintptr(q.texture) == hi {
tmp[w] = q
w += 1
}
}
copy(window, tmp[:w])
}