commit cd40b79306c0b9f057a1c215fd0b9f4e9c5bbf61 Author: Pavel Flegr Date: Fri May 22 16:17:31 2026 +0200 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5dd6a90 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +bin/ +*.exe +*.dll +*.dylib +*.so diff --git a/README.md b/README.md new file mode 100644 index 0000000..c8cc187 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# Odin + SDL3 GPU cubes (Vulkan only) + +Small starter project in Odin that opens an SDL3 window, renders several 3D cubes with the SDL3 GPU API, and uses a Vulkan-only backend with SPIR-V shaders. + +## Controls + +- Left mouse drag: orbit camera +- Middle mouse drag: pan +- Mouse wheel: zoom in/out +- `W/A/S/D`: move camera target +- `Esc`: quit + +## Requirements + +- [Odin compiler](https://odin-lang.org/) +- SDL3 installed on your system (for runtime linking) +- Vulkan-capable GPU/driver with SDL3 Vulkan support +- `glslc` (from `shaderc`) to compile GLSL to SPIR-V + +On macOS with Homebrew: + +```bash +brew install odin sdl3 shaderc +``` + +## Build and run + +Quick start: + +```bash +./run.sh +``` + +The script compiles shaders, sets `VK_ICD_FILENAMES` to MoltenVK if available, +adds `/opt/homebrew/lib` to `DYLD_FALLBACK_LIBRARY_PATH`, and runs the app. + +Compile shaders first: + +```bash +glslc -fshader-stage=vert shaders/cubes.vert.glsl -o shaders/cubes.vert.spv +glslc -fshader-stage=frag shaders/cubes.frag.glsl -o shaders/cubes.frag.spv +``` + +From the project root: + +```bash +odin run src -out:bin/cubes_gpu +``` + +Or build only: + +```bash +odin build src -out:bin/cubes_gpu +``` + +## Notes + +- Shaders source files are `shaders/cubes.vert.glsl` and `shaders/cubes.frag.glsl`. +- Runtime shader binaries are `shaders/cubes.vert.spv` and `shaders/cubes.frag.spv`. +- The app forces SDL3 GPU driver `vulkan` and exits if Vulkan is unavailable. diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..add4668 --- /dev/null +++ b/build.sh @@ -0,0 +1,3 @@ +glslc -fshader-stage=vert shaders/cubes.vert.glsl -o shaders/cubes.vert.spv +glslc -fshader-stage=frag shaders/cubes.frag.glsl -o shaders/cubes.frag.spv +odin run src -out:bin/cubes \ No newline at end of file diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..7bdd24c --- /dev/null +++ b/run.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$ROOT_DIR" + +if ! command -v glslc >/dev/null 2>&1; then + echo "error: glslc not found. Install with: brew install shaderc" + exit 1 +fi + +if ! command -v odin >/dev/null 2>&1; then + echo "error: odin not found. Install with: brew install odin" + exit 1 +fi + +if [[ -z "${VK_ICD_FILENAMES:-}" ]]; then + if [[ -f "/opt/homebrew/etc/vulkan/icd.d/MoltenVK_icd.json" ]]; then + export VK_ICD_FILENAMES="/opt/homebrew/etc/vulkan/icd.d/MoltenVK_icd.json" + elif [[ -f "/opt/homebrew/Cellar/molten-vk/1.4.1/etc/vulkan/icd.d/MoltenVK_icd.json" ]]; then + export VK_ICD_FILENAMES="/opt/homebrew/Cellar/molten-vk/1.4.1/etc/vulkan/icd.d/MoltenVK_icd.json" + fi +fi + +if [[ -z "${DYLD_FALLBACK_LIBRARY_PATH:-}" ]]; then + export DYLD_FALLBACK_LIBRARY_PATH="/opt/homebrew/lib" +elif [[ ":${DYLD_FALLBACK_LIBRARY_PATH}:" != *":/opt/homebrew/lib:"* ]]; then + export DYLD_FALLBACK_LIBRARY_PATH="/opt/homebrew/lib:${DYLD_FALLBACK_LIBRARY_PATH}" +fi + +mkdir -p "bin" + +glslc -fshader-stage=vert "shaders/cubes.vert.glsl" -o "shaders/cubes.vert.spv" +glslc -fshader-stage=frag "shaders/cubes.frag.glsl" -o "shaders/cubes.frag.spv" + +odin build src -out:bin/cubes_vulkan +./bin/cubes_vulkan diff --git a/shaders/cubes.frag.glsl b/shaders/cubes.frag.glsl new file mode 100644 index 0000000..1b2abfd --- /dev/null +++ b/shaders/cubes.frag.glsl @@ -0,0 +1,12 @@ +#version 450 + +layout(location = 0) out vec4 out_color; + +layout(set = 3, binding = 0) uniform FragmentUniforms { + mat4 u_mvp; + vec4 u_color; +}; + +void main() { + out_color = u_color; +} diff --git a/shaders/cubes.frag.spv b/shaders/cubes.frag.spv new file mode 100644 index 0000000..883b7ee Binary files /dev/null and b/shaders/cubes.frag.spv differ diff --git a/shaders/cubes.vert.glsl b/shaders/cubes.vert.glsl new file mode 100644 index 0000000..437895a --- /dev/null +++ b/shaders/cubes.vert.glsl @@ -0,0 +1,12 @@ +#version 450 + +layout(location = 0) in vec3 a_pos; + +layout(set = 1, binding = 0) uniform VertexUniforms { + mat4 u_mvp; + vec4 u_color; +}; + +void main() { + gl_Position = u_mvp * vec4(a_pos, 1.0); +} diff --git a/shaders/cubes.vert.spv b/shaders/cubes.vert.spv new file mode 100644 index 0000000..7db0df5 Binary files /dev/null and b/shaders/cubes.vert.spv differ diff --git a/src/main.odin b/src/main.odin new file mode 100644 index 0000000..e03e621 --- /dev/null +++ b/src/main.odin @@ -0,0 +1,642 @@ +package main + +import "core:c" +import "core:fmt" +import "core:math" +import "core:mem" +import "core:os" +import "core:time" + +import sdl "vendor:sdl3" + +Vec3 :: [3]f32 +Mat4 :: [16]f32 // column-major + +Camera :: struct { + center: Vec3, + distance: f32, + orientation: Rotor, + fov_deg: f32, +} + +Debug_State :: struct { + enabled: bool, + accum: f32, +} + +Rotor :: struct { + s, x, y, z: f32, +} + +Cube_Instance :: struct { + pos: Vec3, + scale: f32, + color: Vec3, +} + +Vertex :: struct { + pos: Vec3, +} + +Push_Constants :: struct { + mvp: Mat4, + color: [4]f32, +} + +mat4_identity :: proc() -> Mat4 { + return Mat4{1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1} +} + +mat4_mul :: proc(a, b: Mat4) -> Mat4 { + r := Mat4{} + for c in 0 ..< 4 { + for row in 0 ..< 4 { + r[c*4+row] = + a[0*4+row] * b[c*4+0] + + a[1*4+row] * b[c*4+1] + + a[2*4+row] * b[c*4+2] + + a[3*4+row] * b[c*4+3] + } + } + return r +} + +vec3_sub :: proc(a, b: Vec3) -> Vec3 { + return Vec3{a[0] - b[0], a[1] - b[1], a[2] - b[2]} +} + +vec3_add :: proc(a, b: Vec3) -> Vec3 { + return Vec3{a[0] + b[0], a[1] + b[1], a[2] + b[2]} +} + +vec3_scale :: proc(v: Vec3, s: f32) -> Vec3 { + return Vec3{v[0] * s, v[1] * s, v[2] * s} +} + +vec3_dot :: proc(a, b: Vec3) -> f32 { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] +} + +vec3_cross :: proc(a, b: Vec3) -> Vec3 { + return Vec3{ + a[1] * b[2] - a[2] * b[1], + a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0], + } +} + +vec3_normalize :: proc(v: Vec3) -> Vec3 { + len2 := vec3_dot(v, v) + if len2 <= 0.000001 { + return Vec3{0, 0, 0} + } + inv := 1.0 / math.sqrt(len2) + return vec3_scale(v, inv) +} + +rotor_identity :: proc() -> Rotor { + return Rotor{1, 0, 0, 0} +} + +rotor_normalize :: proc(r: Rotor) -> Rotor { + len2 := r.s*r.s + r.x*r.x + r.y*r.y + r.z*r.z + if len2 <= 0.000001 { + return rotor_identity() + } + inv := 1.0 / math.sqrt(len2) + return Rotor{r.s * inv, r.x * inv, r.y * inv, r.z * inv} +} + +rotor_mul :: proc(a, b: Rotor) -> Rotor { + return Rotor{ + a.s*b.s - a.x*b.x - a.y*b.y - a.z*b.z, + a.s*b.x + a.x*b.s + a.y*b.z - a.z*b.y, + a.s*b.y - a.x*b.z + a.y*b.s + a.z*b.x, + a.s*b.z + a.x*b.y - a.y*b.x + a.z*b.s, + } +} + +rotor_from_axis_angle :: proc(axis: Vec3, angle: f32) -> Rotor { + a := vec3_normalize(axis) + h := angle * 0.5 + c := math.cos(h) + s := math.sin(h) + return Rotor{c, a[0] * s, a[1] * s, a[2] * s} +} + +rotate_vec3 :: proc(r: Rotor, v: Vec3) -> Vec3 { + u := Vec3{r.x, r.y, r.z} + t := vec3_scale(vec3_cross(u, v), 2.0) + return vec3_add(v, vec3_add(vec3_scale(t, r.s), vec3_cross(u, t))) +} + +mat4_translate :: proc(p: Vec3) -> Mat4 { + m := mat4_identity() + m[12] = p[0] + m[13] = p[1] + m[14] = p[2] + return m +} + +mat4_scale_uniform :: proc(s: f32) -> Mat4 { + m := mat4_identity() + m[0] = s + m[5] = s + m[10] = s + return m +} + +mat4_perspective :: proc(fov_deg, aspect, z_near, z_far: f32) -> Mat4 { + f := 1.0 / math.tan((fov_deg * 0.5) * (math.PI / 180.0)) + m := Mat4{} + m[0] = f / aspect + m[5] = f + m[10] = (z_far + z_near) / (z_near - z_far) + m[11] = -1 + m[14] = (2.0 * z_far * z_near) / (z_near - z_far) + return m +} + +mat4_look_at :: proc(eye, target, up: Vec3) -> Mat4 { + fwd := vec3_normalize(vec3_sub(target, eye)) + right := vec3_normalize(vec3_cross(fwd, up)) + real_up := vec3_cross(right, fwd) + + m := mat4_identity() + m[0] = right[0] + m[1] = real_up[0] + m[2] = -fwd[0] + m[4] = right[1] + m[5] = real_up[1] + m[6] = -fwd[1] + m[8] = right[2] + m[9] = real_up[2] + m[10] = -fwd[2] + m[12] = -vec3_dot(right, eye) + m[13] = -vec3_dot(real_up, eye) + m[14] = vec3_dot(fwd, eye) + return m +} + +camera_forward :: proc(c: Camera) -> Vec3 { + return vec3_normalize(rotate_vec3(c.orientation, Vec3{0, 0, -1})) +} + +camera_right :: proc(c: Camera) -> Vec3 { + return vec3_normalize(rotate_vec3(c.orientation, Vec3{1, 0, 0})) +} + +camera_up :: proc(c: Camera) -> Vec3 { + return vec3_normalize(rotate_vec3(c.orientation, Vec3{0, 1, 0})) +} + +camera_position :: proc(c: Camera) -> Vec3 { + return vec3_sub(c.center, vec3_scale(camera_forward(c), c.distance)) +} + +read_file_or_fail :: proc(path: string) -> []u8 { + file_bytes, err := os.read_entire_file(path, context.allocator) + if err != nil { + fmt.println("failed to read file:", path, "error:", err) + return nil + } + return file_bytes +} + +create_gpu_shader :: proc(device: ^sdl.GPUDevice, path: string, stage: sdl.GPUShaderStage) -> ^sdl.GPUShader { + bytes := read_file_or_fail(path) + if len(bytes) == 0 { + return nil + } + + ci := sdl.GPUShaderCreateInfo{ + code_size = uint(len(bytes)), + code = raw_data(bytes), + entrypoint = cstring("main"), + format = sdl.GPUShaderFormat{.SPIRV}, + stage = stage, + num_samplers = 0, + num_storage_textures = 0, + num_storage_buffers = 0, + num_uniform_buffers = 1, + props = 0, + } + + shader := sdl.CreateGPUShader(device, ci) + delete(bytes) + if shader == nil { + fmt.println("CreateGPUShader failed:", path, "error:", sdl.GetError()) + } + return shader +} + +copy_vertices_to_gpu :: proc(device: ^sdl.GPUDevice, cmd: ^sdl.GPUCommandBuffer, vertices: []Vertex, dst: ^sdl.GPUBuffer) -> bool { + byte_count := len(vertices) * size_of(Vertex) + tb_info := sdl.GPUTransferBufferCreateInfo{ + usage = .UPLOAD, + size = u32(byte_count), + props = 0, + } + tb := sdl.CreateGPUTransferBuffer(device, tb_info) + if tb == nil { + fmt.println("CreateGPUTransferBuffer failed:", sdl.GetError()) + return false + } + defer sdl.ReleaseGPUTransferBuffer(device, tb) + + mapped := sdl.MapGPUTransferBuffer(device, tb, false) + if mapped == nil { + fmt.println("MapGPUTransferBuffer failed:", sdl.GetError()) + return false + } + mem.copy_non_overlapping(mapped, raw_data(vertices), byte_count) + sdl.UnmapGPUTransferBuffer(device, tb) + + cp := sdl.BeginGPUCopyPass(cmd) + if cp == nil { + fmt.println("BeginGPUCopyPass failed:", sdl.GetError()) + return false + } + + src := sdl.GPUTransferBufferLocation{transfer_buffer = tb, offset = 0} + dst_region := sdl.GPUBufferRegion{buffer = dst, offset = 0, size = u32(byte_count)} + sdl.UploadToGPUBuffer(cp, src, dst_region, false) + sdl.EndGPUCopyPass(cp) + return true +} + +main :: proc() { + if !sdl.Init(sdl.INIT_VIDEO) { + fmt.println("SDL init failed:", sdl.GetError()) + return + } + defer sdl.Quit() + + window := sdl.CreateWindow("Odin + SDL3 GPU Cubes", 1280, 720, sdl.WINDOW_RESIZABLE) + if window == nil { + fmt.println("window create failed:", sdl.GetError()) + return + } + defer sdl.DestroyWindow(window) + + device := sdl.CreateGPUDevice(sdl.GPUShaderFormat{.SPIRV}, false, cstring("vulkan")) + if device == nil { + fmt.println("CreateGPUDevice failed:", sdl.GetError()) + return + } + defer sdl.DestroyGPUDevice(device) + + if !sdl.ClaimWindowForGPUDevice(device, window) { + fmt.println("ClaimWindowForGPUDevice failed:", sdl.GetError()) + return + } + defer sdl.ReleaseWindowFromGPUDevice(device, window) + + driver_name := sdl.GetGPUDeviceDriver(device) + fmt.println("SDL GPU driver:", string(driver_name)) + if string(driver_name) != "vulkan" { + fmt.println("expected Vulkan driver, got:", string(driver_name)) + return + } + + vertex_shader := create_gpu_shader(device, "shaders/cubes.vert.spv", .VERTEX) + if vertex_shader == nil { + return + } + defer sdl.ReleaseGPUShader(device, vertex_shader) + + fragment_shader := create_gpu_shader(device, "shaders/cubes.frag.spv", .FRAGMENT) + if fragment_shader == nil { + return + } + defer sdl.ReleaseGPUShader(device, fragment_shader) + + color_format := sdl.GetGPUSwapchainTextureFormat(device, window) + if color_format == .INVALID { + fmt.println("invalid swapchain format:", sdl.GetError()) + return + } + + vb_descs := [1]sdl.GPUVertexBufferDescription{ + {slot = 0, pitch = size_of(Vertex), input_rate = .VERTEX, instance_step_rate = 0}, + } + vattrs := [1]sdl.GPUVertexAttribute{ + {location = 0, buffer_slot = 0, format = .FLOAT3, offset = 0}, + } + + blend := sdl.GPUColorTargetBlendState{ + src_color_blendfactor = .ONE, + dst_color_blendfactor = .ZERO, + color_blend_op = .ADD, + src_alpha_blendfactor = .ONE, + dst_alpha_blendfactor = .ZERO, + alpha_blend_op = .ADD, + color_write_mask = sdl.GPUColorComponentFlags{.R, .G, .B, .A}, + enable_blend = false, + enable_color_write_mask = true, + } + color_targets := [1]sdl.GPUColorTargetDescription{ + {format = color_format, blend_state = blend}, + } + + pipeline_ci := sdl.GPUGraphicsPipelineCreateInfo{ + vertex_shader = vertex_shader, + fragment_shader = fragment_shader, + vertex_input_state = sdl.GPUVertexInputState{ + vertex_buffer_descriptions = &vb_descs[0], + num_vertex_buffers = 1, + vertex_attributes = &vattrs[0], + num_vertex_attributes = 1, + }, + primitive_type = .TRIANGLELIST, + rasterizer_state = sdl.GPURasterizerState{ + fill_mode = .FILL, + cull_mode = .BACK, + front_face = .COUNTER_CLOCKWISE, + depth_bias_constant_factor = 0, + depth_bias_clamp = 0, + depth_bias_slope_factor = 0, + enable_depth_bias = false, + enable_depth_clip = true, + }, + multisample_state = sdl.GPUMultisampleState{ + sample_count = ._1, + sample_mask = 0, + enable_mask = false, + enable_alpha_to_coverage = false, + }, + depth_stencil_state = sdl.GPUDepthStencilState{ + compare_op = .LESS, + back_stencil_state = sdl.GPUStencilOpState{fail_op = .KEEP, pass_op = .KEEP, depth_fail_op = .KEEP, compare_op = .ALWAYS}, + front_stencil_state = sdl.GPUStencilOpState{fail_op = .KEEP, pass_op = .KEEP, depth_fail_op = .KEEP, compare_op = .ALWAYS}, + compare_mask = 0, + write_mask = 0, + enable_depth_test = false, + enable_depth_write = false, + enable_stencil_test = false, + }, + target_info = sdl.GPUGraphicsPipelineTargetInfo{ + color_target_descriptions = &color_targets[0], + num_color_targets = 1, + depth_stencil_format = .INVALID, + has_depth_stencil_target = false, + }, + props = 0, + } + + pipeline := sdl.CreateGPUGraphicsPipeline(device, pipeline_ci) + if pipeline == nil { + fmt.println("CreateGPUGraphicsPipeline failed:", sdl.GetError()) + return + } + defer sdl.ReleaseGPUGraphicsPipeline(device, pipeline) + + vertices := [36]Vertex{ + {pos = Vec3{-0.5, -0.5, -0.5}}, {pos = Vec3{0.5, 0.5, -0.5}}, {pos = Vec3{0.5, -0.5, -0.5}}, + {pos = Vec3{-0.5, -0.5, -0.5}}, {pos = Vec3{-0.5, 0.5, -0.5}}, {pos = Vec3{0.5, 0.5, -0.5}}, + {pos = Vec3{-0.5, -0.5, 0.5}}, {pos = Vec3{0.5, -0.5, 0.5}}, {pos = Vec3{0.5, 0.5, 0.5}}, + {pos = Vec3{-0.5, -0.5, 0.5}}, {pos = Vec3{0.5, 0.5, 0.5}}, {pos = Vec3{-0.5, 0.5, 0.5}}, + {pos = Vec3{-0.5, 0.5, 0.5}}, {pos = Vec3{-0.5, 0.5, -0.5}}, {pos = Vec3{-0.5, -0.5, -0.5}}, + {pos = Vec3{-0.5, -0.5, -0.5}}, {pos = Vec3{-0.5, -0.5, 0.5}}, {pos = Vec3{-0.5, 0.5, 0.5}}, + {pos = Vec3{0.5, 0.5, 0.5}}, {pos = Vec3{0.5, -0.5, -0.5}}, {pos = Vec3{0.5, 0.5, -0.5}}, + {pos = Vec3{0.5, -0.5, -0.5}}, {pos = Vec3{0.5, 0.5, 0.5}}, {pos = Vec3{0.5, -0.5, 0.5}}, + {pos = Vec3{-0.5, -0.5, -0.5}}, {pos = Vec3{0.5, -0.5, -0.5}}, {pos = Vec3{0.5, -0.5, 0.5}}, + {pos = Vec3{-0.5, -0.5, -0.5}}, {pos = Vec3{0.5, -0.5, 0.5}}, {pos = Vec3{-0.5, -0.5, 0.5}}, + {pos = Vec3{-0.5, 0.5, -0.5}}, {pos = Vec3{0.5, 0.5, 0.5}}, {pos = Vec3{0.5, 0.5, -0.5}}, + {pos = Vec3{-0.5, 0.5, -0.5}}, {pos = Vec3{-0.5, 0.5, 0.5}}, {pos = Vec3{0.5, 0.5, 0.5}}, + } + + vbuf_info := sdl.GPUBufferCreateInfo{ + usage = sdl.GPUBufferUsageFlags{.VERTEX}, + size = u32(len(vertices) * size_of(Vertex)), + props = 0, + } + vbuf := sdl.CreateGPUBuffer(device, vbuf_info) + if vbuf == nil { + fmt.println("CreateGPUBuffer failed:", sdl.GetError()) + return + } + defer sdl.ReleaseGPUBuffer(device, vbuf) + + { + setup_cmd := sdl.AcquireGPUCommandBuffer(device) + if setup_cmd == nil { + fmt.println("AcquireGPUCommandBuffer failed:", sdl.GetError()) + return + } + if !copy_vertices_to_gpu(device, setup_cmd, vertices[:], vbuf) { + _ = sdl.CancelGPUCommandBuffer(setup_cmd) + return + } + if !sdl.SubmitGPUCommandBuffer(setup_cmd) { + fmt.println("SubmitGPUCommandBuffer (setup) failed:", sdl.GetError()) + return + } + } + + init_yaw := rotor_from_axis_angle(Vec3{0, 1, 0}, 0.9) + init_right := rotate_vec3(init_yaw, Vec3{1, 0, 0}) + init_pitch := rotor_from_axis_angle(init_right, -0.45) + + camera := Camera{ + center = Vec3{0, 0.5, 0}, + distance = 8, + orientation = rotor_normalize(rotor_mul(init_pitch, init_yaw)), + fov_deg = 60, + } + + cubes := []Cube_Instance{ + {pos = Vec3{0, 0, 0}, scale = 1.0, color = Vec3{0.95, 0.45, 0.20}}, + {pos = Vec3{2, 0.5, -1}, scale = 0.8, color = Vec3{0.20, 0.70, 0.95}}, + {pos = Vec3{-2, -0.2, 1.5}, scale = 1.2, color = Vec3{0.85, 0.85, 0.30}}, + {pos = Vec3{1.0, 1.4, 2.0}, scale = 0.6, color = Vec3{0.40, 0.95, 0.60}}, + } + + running := true + last := time.tick_now() + left_down := false + middle_down := false + debug := Debug_State{} + + for running { + now := time.tick_now() + dt := f32(time.duration_seconds(time.tick_diff(last, now))) + last = now + + event: sdl.Event + for sdl.PollEvent(&event) { + #partial switch event.type { + case .QUIT: + running = false + case .MOUSE_BUTTON_DOWN: + switch event.button.button { + case sdl.BUTTON_LEFT: + left_down = true + case sdl.BUTTON_MIDDLE: + middle_down = true + } + case .MOUSE_BUTTON_UP: + switch event.button.button { + case sdl.BUTTON_LEFT: + left_down = false + case sdl.BUTTON_MIDDLE: + middle_down = false + } + case .MOUSE_WHEEL: + camera.distance = math.clamp(camera.distance - event.wheel.y * 0.7, 1.5, 80.0) + case .KEY_DOWN: + if event.key.key == sdl.K_ESCAPE { + running = false + } else if event.key.scancode == sdl.Scancode.F1 && !event.key.repeat { + debug.enabled = !debug.enabled + fmt.println("debug:", debug.enabled) + } + } + } + + x_rel: f32 = 0 + y_rel: f32 = 0 + _ = sdl.GetRelativeMouseState(&x_rel, &y_rel) + + if left_down { + yaw_r := rotor_from_axis_angle(Vec3{0, 1, 0}, -x_rel * 0.006) + q1 := rotor_normalize(rotor_mul(yaw_r, camera.orientation)) + + right_axis := vec3_normalize(rotate_vec3(q1, Vec3{1, 0, 0})) + pitch_r := rotor_from_axis_angle(right_axis, -y_rel * 0.006) + q2 := rotor_normalize(rotor_mul(pitch_r, q1)) + + fwd2 := camera_forward(Camera{center = camera.center, distance = camera.distance, orientation = q2, fov_deg = camera.fov_deg}) + if math.abs(vec3_dot(fwd2, Vec3{0, 1, 0})) < 0.98 { + camera.orientation = q2 + } else { + camera.orientation = q1 + } + } + + if middle_down { + right := camera_right(camera) + up := camera_up(camera) + pan_speed := 0.008 * camera.distance + camera.center = vec3_add(camera.center, vec3_scale(right, -x_rel * pan_speed)) + camera.center = vec3_add(camera.center, vec3_scale(up, y_rel * pan_speed)) + } + + keys := sdl.GetKeyboardState(nil) + forward := camera_forward(camera) + right := camera_right(camera) + up := camera_up(camera) + move_speed := camera.distance * dt * 1.4 + + if keys[sdl.Scancode.W] { + camera.center = vec3_add(camera.center, vec3_scale(forward, move_speed)) + } + if keys[sdl.Scancode.S] { + camera.center = vec3_add(camera.center, vec3_scale(forward, -move_speed)) + } + if keys[sdl.Scancode.A] { + camera.center = vec3_add(camera.center, vec3_scale(right, -move_speed)) + } + if keys[sdl.Scancode.D] { + camera.center = vec3_add(camera.center, vec3_scale(right, move_speed)) + } + + if debug.enabled { + debug.accum += dt + if debug.accum >= 0.2 { + debug.accum = 0 + eye := camera_position(camera) + fmt.println("cam eye:", eye, + "center:", camera.center, + "fwd:", forward, + "right:", right, + "up:", up) + } + } + + w: c.int = 0 + h: c.int = 0 + sdl.GetWindowSize(window, &w, &h) + if w <= 0 || h <= 0 { + continue + } + + cmd := sdl.AcquireGPUCommandBuffer(device) + if cmd == nil { + fmt.println("AcquireGPUCommandBuffer failed:", sdl.GetError()) + break + } + + swap_tex: ^sdl.GPUTexture + swap_w: sdl.Uint32 = 0 + swap_h: sdl.Uint32 = 0 + if !sdl.WaitAndAcquireGPUSwapchainTexture(cmd, window, &swap_tex, &swap_w, &swap_h) { + fmt.println("WaitAndAcquireGPUSwapchainTexture failed:", sdl.GetError()) + _ = sdl.CancelGPUCommandBuffer(cmd) + break + } + + if swap_tex == nil { + if !sdl.SubmitGPUCommandBuffer(cmd) { + fmt.println("SubmitGPUCommandBuffer failed:", sdl.GetError()) + break + } + continue + } + + clear := sdl.FColor{0.08, 0.09, 0.12, 1.0} + cti := sdl.GPUColorTargetInfo{ + texture = swap_tex, + mip_level = 0, + layer_or_depth_plane = 0, + clear_color = clear, + load_op = .CLEAR, + store_op = .STORE, + resolve_texture = nil, + resolve_mip_level = 0, + resolve_layer = 0, + cycle = false, + cycle_resolve_texture = false, + } + + rp := sdl.BeginGPURenderPass(cmd, &cti, 1, nil) + if rp == nil { + fmt.println("BeginGPURenderPass failed:", sdl.GetError()) + _ = sdl.CancelGPUCommandBuffer(cmd) + break + } + + vp := sdl.GPUViewport{x = 0, y = 0, w = f32(swap_w), h = f32(swap_h), min_depth = 0, max_depth = 1} + sdl.SetGPUViewport(rp, vp) + sdl.BindGPUGraphicsPipeline(rp, pipeline) + vb_binding := sdl.GPUBufferBinding{buffer = vbuf, offset = 0} + sdl.BindGPUVertexBuffers(rp, 0, &vb_binding, 1) + + aspect := f32(swap_w) / f32(swap_h) + proj := mat4_perspective(camera.fov_deg, aspect, 0.1, 300.0) + view := mat4_look_at(camera_position(camera), camera.center, camera_up(camera)) + vp_mat := mat4_mul(proj, view) + + for cube in cubes { + model := mat4_mul(mat4_translate(cube.pos), mat4_scale_uniform(cube.scale)) + pc := Push_Constants{} + pc.mvp = mat4_mul(vp_mat, model) + pc.color = [4]f32{cube.color[0], cube.color[1], cube.color[2], 1.0} + sdl.PushGPUVertexUniformData(cmd, 0, &pc, u32(size_of(Push_Constants))) + sdl.PushGPUFragmentUniformData(cmd, 0, &pc, u32(size_of(Push_Constants))) + sdl.DrawGPUPrimitives(rp, 36, 1, 0, 0) + } + + sdl.EndGPURenderPass(rp) + + if !sdl.SubmitGPUCommandBuffer(cmd) { + fmt.println("SubmitGPUCommandBuffer failed:", sdl.GetError()) + break + } + } + + _ = sdl.WaitForGPUIdle(device) +}