From 38f296eccefff14679ac74ed48da043eed74b22a Mon Sep 17 00:00:00 2001 From: Stephen Gutekanst Date: Mon, 4 Mar 2024 23:25:11 -0700 Subject: [PATCH] src/core: move mach-core@9a4d09707d9f1cb6ea5602bdf58caeefc46146be package to here Helps hexops/mach#1165 Signed-off-by: Stephen Gutekanst --- src/core/Frequency.zig | 66 + src/core/InputState.zig | 25 + src/core/Timer.zig | 38 + src/core/examples/LICENSE.webgpu-samples | 26 + src/core/examples/boids/main.zig | 263 +++ src/core/examples/boids/sprite.wgsl | 15 + src/core/examples/boids/updateSprites.wgsl | 90 + src/core/examples/clear-color/main.zig | 78 + src/core/examples/clear-color/renderer.zig | 32 + src/core/examples/cubemap/cube_mesh.zig | 49 + src/core/examples/cubemap/main.zig | 392 ++++ src/core/examples/cubemap/shader.wgsl | 34 + .../fragmentDeferredRendering.wgsl | 83 + .../fragmentGBuffersDebugView.wgsl | 44 + .../fragmentWriteGBuffers.wgsl | 22 + .../deferred-rendering/lightUpdate.wgsl | 34 + src/core/examples/deferred-rendering/main.zig | 1210 +++++++++++++ .../deferred-rendering/vertexTextureQuad.wgsl | 11 + .../vertexWriteGBuffers.wgsl | 30 + .../deferred-rendering/vertex_writer.zig | 188 ++ src/core/examples/fractal-cube/cube_mesh.zig | 49 + src/core/examples/fractal-cube/main.zig | 371 ++++ src/core/examples/fractal-cube/shader.wgsl | 36 + src/core/examples/gen-texture-light/cube.wgsl | 75 + .../examples/gen-texture-light/light.wgsl | 35 + src/core/examples/gen-texture-light/main.zig | 891 +++++++++ src/core/examples/image-blur/blur.wgsl | 81 + .../image-blur/fullscreen_textured_quad.wgsl | 38 + src/core/examples/image-blur/main.zig | 326 ++++ .../image/fullscreen_textured_quad.wgsl | 39 + src/core/examples/image/main.zig | 184 ++ .../examples/instanced-cube/cube_mesh.zig | 49 + src/core/examples/instanced-cube/main.zig | 205 +++ src/core/examples/instanced-cube/shader.wgsl | 25 + src/core/examples/map-async/main.wgsl | 16 + src/core/examples/map-async/main.zig | 101 ++ src/core/examples/pbr-basic/main.zig | 920 ++++++++++ src/core/examples/pbr-basic/shader.wgsl | 118 ++ src/core/examples/pbr-basic/vertex_writer.zig | 188 ++ .../examples/pixel-post-process/cube_mesh.zig | 49 + src/core/examples/pixel-post-process/main.zig | 461 +++++ .../pixel-post-process/normal_frag.wgsl | 6 + .../pixel-post-process/pixel_frag.wgsl | 74 + .../pixel-post-process/pixel_vert.wgsl | 14 + .../examples/pixel-post-process/quad_mesh.zig | 13 + .../examples/pixel-post-process/shader.wgsl | 27 + .../examples/procedural-primitives/main.zig | 62 + .../procedural-primitives.zig | 336 ++++ .../procedural-primitives/renderer.zig | 325 ++++ .../procedural-primitives/shader.wgsl | 33 + src/core/examples/rgb-quad/main.zig | 138 ++ src/core/examples/rgb-quad/shader.wgsl | 15 + src/core/examples/rotating-cube/cube_mesh.zig | 49 + src/core/examples/rotating-cube/main.zig | 192 ++ src/core/examples/rotating-cube/shader.wgsl | 24 + src/core/examples/sprite2d/main.zig | 354 ++++ src/core/examples/sprite2d/sprite-shader.wgsl | 82 + src/core/examples/sprite2d/sprites.json | 53 + src/core/examples/sysgpu/boids/main.zig | 268 +++ src/core/examples/sysgpu/boids/sprite.wgsl | 15 + .../examples/sysgpu/boids/updateSprites.wgsl | 90 + src/core/examples/sysgpu/clear-color/main.zig | 83 + .../examples/sysgpu/clear-color/renderer.zig | 32 + .../examples/sysgpu/cubemap/cube_mesh.zig | 49 + src/core/examples/sysgpu/cubemap/main.zig | 397 ++++ src/core/examples/sysgpu/cubemap/shader.wgsl | 34 + .../fragmentDeferredRendering.wgsl | 89 + .../fragmentGBuffersDebugView.wgsl | 44 + .../fragmentWriteGBuffers.wgsl | 22 + .../deferred-rendering/lightUpdate.wgsl | 37 + .../sysgpu/deferred-rendering/main.zig | 1224 +++++++++++++ .../deferred-rendering/vertexTextureQuad.wgsl | 12 + .../vertexWriteGBuffers.wgsl | 30 + .../deferred-rendering/vertex_writer.zig | 188 ++ .../sysgpu/fractal-cube/cube_mesh.zig | 49 + .../examples/sysgpu/fractal-cube/main.zig | 376 ++++ .../examples/sysgpu/fractal-cube/shader.wgsl | 36 + .../sysgpu/gen-texture-light/cube.wgsl | 71 + .../sysgpu/gen-texture-light/light.wgsl | 34 + .../sysgpu/gen-texture-light/main.zig | 896 +++++++++ src/core/examples/sysgpu/image-blur/blur.wgsl | 82 + .../image-blur/fullscreen_textured_quad.wgsl | 38 + src/core/examples/sysgpu/image-blur/main.zig | 331 ++++ .../image/fullscreen_textured_quad.wgsl | 39 + src/core/examples/sysgpu/image/main.zig | 189 ++ .../sysgpu/instanced-cube/cube_mesh.zig | 49 + .../examples/sysgpu/instanced-cube/main.zig | 210 +++ .../sysgpu/instanced-cube/shader.wgsl | 25 + src/core/examples/sysgpu/map-async/main.wgsl | 16 + src/core/examples/sysgpu/map-async/main.zig | 106 ++ src/core/examples/sysgpu/pbr-basic/main.zig | 931 ++++++++++ .../examples/sysgpu/pbr-basic/shader.wgsl | 119 ++ .../sysgpu/pbr-basic/vertex_writer.zig | 188 ++ .../sysgpu/pixel-post-process/cube_mesh.zig | 49 + .../sysgpu/pixel-post-process/main.zig | 466 +++++ .../pixel-post-process/normal_frag.wgsl | 6 + .../sysgpu/pixel-post-process/pixel_frag.wgsl | 77 + .../sysgpu/pixel-post-process/pixel_vert.wgsl | 14 + .../sysgpu/pixel-post-process/quad_mesh.zig | 13 + .../sysgpu/pixel-post-process/shader.wgsl | 27 + .../sysgpu/procedural-primitives/main.zig | 66 + .../procedural-primitives.zig | 336 ++++ .../sysgpu/procedural-primitives/renderer.zig | 325 ++++ .../sysgpu/procedural-primitives/shader.wgsl | 32 + src/core/examples/sysgpu/rgb-quad/main.zig | 143 ++ src/core/examples/sysgpu/rgb-quad/shader.wgsl | 15 + .../sysgpu/rotating-cube/cube_mesh.zig | 49 + .../examples/sysgpu/rotating-cube/main.zig | 197 ++ .../examples/sysgpu/rotating-cube/shader.wgsl | 24 + src/core/examples/sysgpu/sprite2d/main.zig | 359 ++++ .../sysgpu/sprite2d/sprite-shader.wgsl | 82 + .../examples/sysgpu/sprite2d/sprites.json | 53 + .../sysgpu/textured-cube/cube_mesh.zig | 49 + .../examples/sysgpu/textured-cube/main.zig | 318 ++++ .../examples/sysgpu/textured-cube/shader.wgsl | 29 + .../examples/sysgpu/textured-quad/main.zig | 209 +++ .../examples/sysgpu/textured-quad/shader.wgsl | 21 + .../examples/sysgpu/triangle-msaa/main.zig | 133 ++ .../examples/sysgpu/triangle-msaa/shader.wgsl | 14 + src/core/examples/sysgpu/triangle/main.zig | 96 + src/core/examples/sysgpu/triangle/shader.wgsl | 14 + .../examples/sysgpu/two-cubes/cube_mesh.zig | 49 + src/core/examples/sysgpu/two-cubes/main.zig | 228 +++ .../examples/sysgpu/two-cubes/shader.wgsl | 24 + src/core/examples/textured-cube/cube_mesh.zig | 49 + src/core/examples/textured-cube/main.zig | 313 ++++ src/core/examples/textured-cube/shader.wgsl | 29 + src/core/examples/textured-quad/main.zig | 204 +++ src/core/examples/textured-quad/shader.wgsl | 21 + src/core/examples/triangle-msaa/main.zig | 128 ++ src/core/examples/triangle-msaa/shader.wgsl | 14 + src/core/examples/triangle/main.zig | 91 + src/core/examples/triangle/shader.wgsl | 14 + src/core/examples/two-cubes/cube_mesh.zig | 49 + src/core/examples/two-cubes/main.zig | 223 +++ src/core/examples/two-cubes/shader.wgsl | 24 + src/core/examples/wasm-test/main.zig | 35 + src/core/examples/zmath.zig | 996 ++++++++++ src/core/main.zig | 761 ++++++++ src/core/platform.zig | 74 + src/core/platform/common.zig | 89 + src/core/platform/glfw.zig | 4 + src/core/platform/glfw/Core.zig | 1363 ++++++++++++++ src/core/platform/glfw/objc.zig | 57 + src/core/platform/native_entrypoint.zig | 39 + src/core/platform/wasm.zig | 2 + src/core/platform/wasm/Core.zig | 478 +++++ src/core/platform/wasm/Timer.zig | 25 + src/core/platform/wasm/entrypoint.zig | 42 + src/core/platform/wasm/js.zig | 42 + src/core/platform/wasm/mach.js | 569 ++++++ src/core/platform/wayland.zig | 4 + src/core/platform/wayland/Core.zig | 1459 +++++++++++++++ src/core/platform/wayland/wayland.c | 7 + src/core/platform/x11.zig | 4 + src/core/platform/x11/Core.zig | 1609 +++++++++++++++++ src/core/platform/x11/unicode.zig | 865 +++++++++ 157 files changed, 28383 insertions(+) create mode 100644 src/core/Frequency.zig create mode 100644 src/core/InputState.zig create mode 100644 src/core/Timer.zig create mode 100644 src/core/examples/LICENSE.webgpu-samples create mode 100644 src/core/examples/boids/main.zig create mode 100644 src/core/examples/boids/sprite.wgsl create mode 100644 src/core/examples/boids/updateSprites.wgsl create mode 100644 src/core/examples/clear-color/main.zig create mode 100644 src/core/examples/clear-color/renderer.zig create mode 100644 src/core/examples/cubemap/cube_mesh.zig create mode 100644 src/core/examples/cubemap/main.zig create mode 100644 src/core/examples/cubemap/shader.wgsl create mode 100644 src/core/examples/deferred-rendering/fragmentDeferredRendering.wgsl create mode 100644 src/core/examples/deferred-rendering/fragmentGBuffersDebugView.wgsl create mode 100644 src/core/examples/deferred-rendering/fragmentWriteGBuffers.wgsl create mode 100644 src/core/examples/deferred-rendering/lightUpdate.wgsl create mode 100644 src/core/examples/deferred-rendering/main.zig create mode 100644 src/core/examples/deferred-rendering/vertexTextureQuad.wgsl create mode 100644 src/core/examples/deferred-rendering/vertexWriteGBuffers.wgsl create mode 100644 src/core/examples/deferred-rendering/vertex_writer.zig create mode 100644 src/core/examples/fractal-cube/cube_mesh.zig create mode 100755 src/core/examples/fractal-cube/main.zig create mode 100644 src/core/examples/fractal-cube/shader.wgsl create mode 100644 src/core/examples/gen-texture-light/cube.wgsl create mode 100644 src/core/examples/gen-texture-light/light.wgsl create mode 100755 src/core/examples/gen-texture-light/main.zig create mode 100644 src/core/examples/image-blur/blur.wgsl create mode 100644 src/core/examples/image-blur/fullscreen_textured_quad.wgsl create mode 100644 src/core/examples/image-blur/main.zig create mode 100644 src/core/examples/image/fullscreen_textured_quad.wgsl create mode 100644 src/core/examples/image/main.zig create mode 100644 src/core/examples/instanced-cube/cube_mesh.zig create mode 100755 src/core/examples/instanced-cube/main.zig create mode 100644 src/core/examples/instanced-cube/shader.wgsl create mode 100644 src/core/examples/map-async/main.wgsl create mode 100644 src/core/examples/map-async/main.zig create mode 100644 src/core/examples/pbr-basic/main.zig create mode 100644 src/core/examples/pbr-basic/shader.wgsl create mode 100644 src/core/examples/pbr-basic/vertex_writer.zig create mode 100644 src/core/examples/pixel-post-process/cube_mesh.zig create mode 100644 src/core/examples/pixel-post-process/main.zig create mode 100644 src/core/examples/pixel-post-process/normal_frag.wgsl create mode 100644 src/core/examples/pixel-post-process/pixel_frag.wgsl create mode 100644 src/core/examples/pixel-post-process/pixel_vert.wgsl create mode 100644 src/core/examples/pixel-post-process/quad_mesh.zig create mode 100644 src/core/examples/pixel-post-process/shader.wgsl create mode 100644 src/core/examples/procedural-primitives/main.zig create mode 100644 src/core/examples/procedural-primitives/procedural-primitives.zig create mode 100644 src/core/examples/procedural-primitives/renderer.zig create mode 100644 src/core/examples/procedural-primitives/shader.wgsl create mode 100644 src/core/examples/rgb-quad/main.zig create mode 100644 src/core/examples/rgb-quad/shader.wgsl create mode 100644 src/core/examples/rotating-cube/cube_mesh.zig create mode 100755 src/core/examples/rotating-cube/main.zig create mode 100644 src/core/examples/rotating-cube/shader.wgsl create mode 100644 src/core/examples/sprite2d/main.zig create mode 100644 src/core/examples/sprite2d/sprite-shader.wgsl create mode 100644 src/core/examples/sprite2d/sprites.json create mode 100644 src/core/examples/sysgpu/boids/main.zig create mode 100644 src/core/examples/sysgpu/boids/sprite.wgsl create mode 100644 src/core/examples/sysgpu/boids/updateSprites.wgsl create mode 100644 src/core/examples/sysgpu/clear-color/main.zig create mode 100644 src/core/examples/sysgpu/clear-color/renderer.zig create mode 100644 src/core/examples/sysgpu/cubemap/cube_mesh.zig create mode 100644 src/core/examples/sysgpu/cubemap/main.zig create mode 100644 src/core/examples/sysgpu/cubemap/shader.wgsl create mode 100644 src/core/examples/sysgpu/deferred-rendering/fragmentDeferredRendering.wgsl create mode 100644 src/core/examples/sysgpu/deferred-rendering/fragmentGBuffersDebugView.wgsl create mode 100644 src/core/examples/sysgpu/deferred-rendering/fragmentWriteGBuffers.wgsl create mode 100644 src/core/examples/sysgpu/deferred-rendering/lightUpdate.wgsl create mode 100644 src/core/examples/sysgpu/deferred-rendering/main.zig create mode 100644 src/core/examples/sysgpu/deferred-rendering/vertexTextureQuad.wgsl create mode 100644 src/core/examples/sysgpu/deferred-rendering/vertexWriteGBuffers.wgsl create mode 100644 src/core/examples/sysgpu/deferred-rendering/vertex_writer.zig create mode 100644 src/core/examples/sysgpu/fractal-cube/cube_mesh.zig create mode 100644 src/core/examples/sysgpu/fractal-cube/main.zig create mode 100644 src/core/examples/sysgpu/fractal-cube/shader.wgsl create mode 100644 src/core/examples/sysgpu/gen-texture-light/cube.wgsl create mode 100644 src/core/examples/sysgpu/gen-texture-light/light.wgsl create mode 100644 src/core/examples/sysgpu/gen-texture-light/main.zig create mode 100644 src/core/examples/sysgpu/image-blur/blur.wgsl create mode 100644 src/core/examples/sysgpu/image-blur/fullscreen_textured_quad.wgsl create mode 100644 src/core/examples/sysgpu/image-blur/main.zig create mode 100644 src/core/examples/sysgpu/image/fullscreen_textured_quad.wgsl create mode 100644 src/core/examples/sysgpu/image/main.zig create mode 100644 src/core/examples/sysgpu/instanced-cube/cube_mesh.zig create mode 100644 src/core/examples/sysgpu/instanced-cube/main.zig create mode 100644 src/core/examples/sysgpu/instanced-cube/shader.wgsl create mode 100644 src/core/examples/sysgpu/map-async/main.wgsl create mode 100644 src/core/examples/sysgpu/map-async/main.zig create mode 100644 src/core/examples/sysgpu/pbr-basic/main.zig create mode 100644 src/core/examples/sysgpu/pbr-basic/shader.wgsl create mode 100644 src/core/examples/sysgpu/pbr-basic/vertex_writer.zig create mode 100644 src/core/examples/sysgpu/pixel-post-process/cube_mesh.zig create mode 100644 src/core/examples/sysgpu/pixel-post-process/main.zig create mode 100644 src/core/examples/sysgpu/pixel-post-process/normal_frag.wgsl create mode 100644 src/core/examples/sysgpu/pixel-post-process/pixel_frag.wgsl create mode 100644 src/core/examples/sysgpu/pixel-post-process/pixel_vert.wgsl create mode 100644 src/core/examples/sysgpu/pixel-post-process/quad_mesh.zig create mode 100644 src/core/examples/sysgpu/pixel-post-process/shader.wgsl create mode 100644 src/core/examples/sysgpu/procedural-primitives/main.zig create mode 100644 src/core/examples/sysgpu/procedural-primitives/procedural-primitives.zig create mode 100644 src/core/examples/sysgpu/procedural-primitives/renderer.zig create mode 100644 src/core/examples/sysgpu/procedural-primitives/shader.wgsl create mode 100644 src/core/examples/sysgpu/rgb-quad/main.zig create mode 100644 src/core/examples/sysgpu/rgb-quad/shader.wgsl create mode 100644 src/core/examples/sysgpu/rotating-cube/cube_mesh.zig create mode 100644 src/core/examples/sysgpu/rotating-cube/main.zig create mode 100644 src/core/examples/sysgpu/rotating-cube/shader.wgsl create mode 100644 src/core/examples/sysgpu/sprite2d/main.zig create mode 100644 src/core/examples/sysgpu/sprite2d/sprite-shader.wgsl create mode 100644 src/core/examples/sysgpu/sprite2d/sprites.json create mode 100644 src/core/examples/sysgpu/textured-cube/cube_mesh.zig create mode 100644 src/core/examples/sysgpu/textured-cube/main.zig create mode 100644 src/core/examples/sysgpu/textured-cube/shader.wgsl create mode 100644 src/core/examples/sysgpu/textured-quad/main.zig create mode 100644 src/core/examples/sysgpu/textured-quad/shader.wgsl create mode 100644 src/core/examples/sysgpu/triangle-msaa/main.zig create mode 100644 src/core/examples/sysgpu/triangle-msaa/shader.wgsl create mode 100644 src/core/examples/sysgpu/triangle/main.zig create mode 100644 src/core/examples/sysgpu/triangle/shader.wgsl create mode 100644 src/core/examples/sysgpu/two-cubes/cube_mesh.zig create mode 100644 src/core/examples/sysgpu/two-cubes/main.zig create mode 100644 src/core/examples/sysgpu/two-cubes/shader.wgsl create mode 100644 src/core/examples/textured-cube/cube_mesh.zig create mode 100644 src/core/examples/textured-cube/main.zig create mode 100644 src/core/examples/textured-cube/shader.wgsl create mode 100644 src/core/examples/textured-quad/main.zig create mode 100644 src/core/examples/textured-quad/shader.wgsl create mode 100644 src/core/examples/triangle-msaa/main.zig create mode 100644 src/core/examples/triangle-msaa/shader.wgsl create mode 100644 src/core/examples/triangle/main.zig create mode 100644 src/core/examples/triangle/shader.wgsl create mode 100644 src/core/examples/two-cubes/cube_mesh.zig create mode 100755 src/core/examples/two-cubes/main.zig create mode 100644 src/core/examples/two-cubes/shader.wgsl create mode 100644 src/core/examples/wasm-test/main.zig create mode 100644 src/core/examples/zmath.zig create mode 100644 src/core/main.zig create mode 100644 src/core/platform.zig create mode 100644 src/core/platform/common.zig create mode 100644 src/core/platform/glfw.zig create mode 100644 src/core/platform/glfw/Core.zig create mode 100644 src/core/platform/glfw/objc.zig create mode 100644 src/core/platform/native_entrypoint.zig create mode 100644 src/core/platform/wasm.zig create mode 100644 src/core/platform/wasm/Core.zig create mode 100644 src/core/platform/wasm/Timer.zig create mode 100644 src/core/platform/wasm/entrypoint.zig create mode 100644 src/core/platform/wasm/js.zig create mode 100644 src/core/platform/wasm/mach.js create mode 100644 src/core/platform/wayland.zig create mode 100644 src/core/platform/wayland/Core.zig create mode 100644 src/core/platform/wayland/wayland.c create mode 100644 src/core/platform/x11.zig create mode 100644 src/core/platform/x11/Core.zig create mode 100644 src/core/platform/x11/unicode.zig diff --git a/src/core/Frequency.zig b/src/core/Frequency.zig new file mode 100644 index 00000000..86219d1b --- /dev/null +++ b/src/core/Frequency.zig @@ -0,0 +1,66 @@ +const std = @import("std"); +const core = @import("main.zig"); +const Timer = @import("Timer.zig"); + +pub const Frequency = @This(); + +/// The target frequency (e.g. 60hz) or zero for unlimited +target: u32 = 0, + +/// The estimated delay that is needed to achieve the target frequency. Updated during tick() +delay_ns: u64 = 0, + +/// The actual measured frequency. This is updated every second. +rate: u32 = 0, + +delta_time: ?*f32 = null, +delta_time_ns: *u64 = undefined, + +/// Internal fields, this must be initialized via a call to start(). +internal: struct { + // The frame number in this second's cycle. e.g. zero to 59 + count: u32, + timer: Timer, + last_time: u64, +} = undefined, + +/// Starts the timer used for frequency calculation. Must be called once before anything else. +pub fn start(f: *Frequency) !void { + f.internal = .{ + .count = 0, + .timer = try Timer.start(), + .last_time = 0, + }; +} + +/// Tick should be called at each occurrence (e.g. frame) +pub inline fn tick(f: *Frequency) void { + var current_time = f.internal.timer.readPrecise(); + + if (f.delta_time) |delta_time| { + f.delta_time_ns.* = current_time -| f.internal.last_time; + delta_time.* = @as(f32, @floatFromInt(f.delta_time_ns.*)) / @as(f32, @floatFromInt(std.time.ns_per_s)); + } + + if (current_time >= std.time.ns_per_s) { + f.rate = f.internal.count; + f.internal.count = 0; + f.internal.timer.reset(); + current_time -= std.time.ns_per_s; + } + f.internal.last_time = current_time; + f.internal.count += 1; + + if (f.target != 0) { + const limited_count = @min(f.target, f.internal.count); + const target_time_per_tick: u64 = (std.time.ns_per_s / f.target); + const target_time = target_time_per_tick * limited_count; + if (current_time > target_time) { + f.delay_ns = 0; + } else { + f.delay_ns = target_time - current_time; + } + } else { + f.delay_ns = 0; + } +} diff --git a/src/core/InputState.zig b/src/core/InputState.zig new file mode 100644 index 00000000..3bfed90b --- /dev/null +++ b/src/core/InputState.zig @@ -0,0 +1,25 @@ +const std = @import("std"); +const core = @import("main.zig"); +const KeyBitSet = std.StaticBitSet(@intFromEnum(core.Key.max) + 1); +const MouseButtonSet = std.StaticBitSet(@as(u4, @intFromEnum(core.MouseButton.max)) + 1); +const InputState = @This(); + +keys: KeyBitSet = KeyBitSet.initEmpty(), +mouse_buttons: MouseButtonSet = MouseButtonSet.initEmpty(), +mouse_position: core.Position = .{ .x = 0, .y = 0 }, + +pub inline fn isKeyPressed(self: InputState, key: core.Key) bool { + return self.keys.isSet(@intFromEnum(key)); +} + +pub inline fn isKeyReleased(self: InputState, key: core.Key) bool { + return !self.isKeyPressed(key); +} + +pub inline fn isMouseButtonPressed(self: InputState, button: core.MouseButton) bool { + return self.mouse_buttons.isSet(@intFromEnum(button)); +} + +pub inline fn isMouseButtonReleased(self: InputState, button: core.MouseButton) bool { + return !self.isMouseButtonPressed(button); +} diff --git a/src/core/Timer.zig b/src/core/Timer.zig new file mode 100644 index 00000000..c84b9f30 --- /dev/null +++ b/src/core/Timer.zig @@ -0,0 +1,38 @@ +const std = @import("std"); +const platform = @import("platform.zig"); + +pub const Timer = @This(); + +internal: platform.Timer, + +/// Initialize the timer. +pub fn start() !Timer { + return Timer{ + .internal = try platform.Timer.start(), + }; +} + +/// Reads the timer value since start or the last reset in nanoseconds. +pub inline fn readPrecise(timer: *Timer) u64 { + return timer.internal.read(); +} + +/// Reads the timer value since start or the last reset in seconds. +pub inline fn read(timer: *Timer) f32 { + return @as(f32, @floatFromInt(timer.readPrecise())) / @as(f32, @floatFromInt(std.time.ns_per_s)); +} + +/// Resets the timer value to 0/now. +pub inline fn reset(timer: *Timer) void { + timer.internal.reset(); +} + +/// Returns the current value of the timer in nanoseconds, then resets it. +pub inline fn lapPrecise(timer: *Timer) u64 { + return timer.internal.lap(); +} + +/// Returns the current value of the timer in seconds, then resets it. +pub inline fn lap(timer: *Timer) f32 { + return @as(f32, @floatFromInt(timer.lapPrecise())) / @as(f32, @floatFromInt(std.time.ns_per_s)); +} diff --git a/src/core/examples/LICENSE.webgpu-samples b/src/core/examples/LICENSE.webgpu-samples new file mode 100644 index 00000000..e7a21bee --- /dev/null +++ b/src/core/examples/LICENSE.webgpu-samples @@ -0,0 +1,26 @@ +Copyright 2019 WebGPU Samples Contributors + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/core/examples/boids/main.zig b/src/core/examples/boids/main.zig new file mode 100644 index 00000000..1f09e8b1 --- /dev/null +++ b/src/core/examples/boids/main.zig @@ -0,0 +1,263 @@ +/// A port of Austin Eng's "computeBoids" webgpu sample. +/// https://github.com/austinEng/webgpu-samples/blob/main/src/sample/computeBoids/main.ts +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; + +title_timer: core.Timer, +timer: core.Timer, +compute_pipeline: *gpu.ComputePipeline, +render_pipeline: *gpu.RenderPipeline, +sprite_vertex_buffer: *gpu.Buffer, +particle_buffers: [2]*gpu.Buffer, +particle_bind_groups: [2]*gpu.BindGroup, +sim_param_buffer: *gpu.Buffer, +frame_counter: usize, + +pub const App = @This(); + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +const num_particle = 1500; + +var sim_params = [_]f32{ + 0.04, // .delta_T + 0.1, // .rule_1_distance + 0.025, // .rule_2_distance + 0.025, // .rule_3_distance + 0.02, // .rule_1_scale + 0.05, // .rule_2_scale + 0.005, // .rule_3_scale +}; + +pub fn init(app: *App) !void { + try core.init(.{}); + + const sprite_shader_module = core.device.createShaderModuleWGSL( + "sprite.wgsl", + @embedFile("sprite.wgsl"), + ); + defer sprite_shader_module.release(); + + const update_sprite_shader_module = core.device.createShaderModuleWGSL( + "updateSprites.wgsl", + @embedFile("updateSprites.wgsl"), + ); + defer update_sprite_shader_module.release(); + + const instanced_particles_attributes = [_]gpu.VertexAttribute{ + .{ + // instance position + .shader_location = 0, + .offset = 0, + .format = .float32x2, + }, + .{ + // instance velocity + .shader_location = 1, + .offset = 2 * 4, + .format = .float32x2, + }, + }; + + const vertex_buffer_attributes = [_]gpu.VertexAttribute{ + .{ + // vertex positions + .shader_location = 2, + .offset = 0, + .format = .float32x2, + }, + }; + + const render_pipeline = core.device.createRenderPipeline(&gpu.RenderPipeline.Descriptor{ + .vertex = gpu.VertexState.init(.{ + .module = sprite_shader_module, + .entry_point = "vert_main", + .buffers = &.{ + gpu.VertexBufferLayout.init(.{ + // instanced particles buffer + .array_stride = 4 * 4, + .step_mode = .instance, + .attributes = &instanced_particles_attributes, + }), + gpu.VertexBufferLayout.init(.{ + // vertex buffer + .array_stride = 2 * 4, + .step_mode = .vertex, + .attributes = &vertex_buffer_attributes, + }), + }, + }), + .fragment = &gpu.FragmentState.init(.{ + .module = sprite_shader_module, + .entry_point = "frag_main", + .targets = &[_]gpu.ColorTargetState{.{ + .format = core.descriptor.format, + }}, + }), + }); + + const compute_pipeline = core.device.createComputePipeline(&gpu.ComputePipeline.Descriptor{ .compute = gpu.ProgrammableStageDescriptor{ + .module = update_sprite_shader_module, + .entry_point = "main", + } }); + + const vert_buffer_data = [_]f32{ + -0.01, -0.02, 0.01, + -0.02, 0.0, 0.02, + }; + + const sprite_vertex_buffer = core.device.createBuffer(&gpu.Buffer.Descriptor{ + .label = "sprite_vertex_buffer", + .usage = .{ .vertex = true }, + .mapped_at_creation = .true, + .size = vert_buffer_data.len * @sizeOf(f32), + }); + const vertex_mapped = sprite_vertex_buffer.getMappedRange(f32, 0, vert_buffer_data.len); + @memcpy(vertex_mapped.?, vert_buffer_data[0..]); + sprite_vertex_buffer.unmap(); + + const sim_param_buffer = core.device.createBuffer(&gpu.Buffer.Descriptor{ + .label = "sim_param_buffer", + .usage = .{ .uniform = true, .copy_dst = true }, + .size = sim_params.len * @sizeOf(f32), + }); + core.queue.writeBuffer(sim_param_buffer, 0, sim_params[0..]); + + var initial_particle_data: [num_particle * 4]f32 = undefined; + var rng = std.rand.DefaultPrng.init(0); + const random = rng.random(); + var i: usize = 0; + while (i < num_particle) : (i += 1) { + initial_particle_data[4 * i + 0] = 2 * (random.float(f32) - 0.5); + initial_particle_data[4 * i + 1] = 2 * (random.float(f32) - 0.5); + initial_particle_data[4 * i + 2] = 2 * (random.float(f32) - 0.5) * 0.1; + initial_particle_data[4 * i + 3] = 2 * (random.float(f32) - 0.5) * 0.1; + } + + var particle_buffers: [2]*gpu.Buffer = undefined; + var particle_bind_groups: [2]*gpu.BindGroup = undefined; + i = 0; + while (i < 2) : (i += 1) { + particle_buffers[i] = core.device.createBuffer(&gpu.Buffer.Descriptor{ + .label = "particle_buffer", + .mapped_at_creation = .true, + .usage = .{ + .vertex = true, + .storage = true, + }, + .size = initial_particle_data.len * @sizeOf(f32), + }); + const mapped = particle_buffers[i].getMappedRange(f32, 0, initial_particle_data.len); + @memcpy(mapped.?, initial_particle_data[0..]); + particle_buffers[i].unmap(); + } + + i = 0; + while (i < 2) : (i += 1) { + const layout = compute_pipeline.getBindGroupLayout(0); + defer layout.release(); + + particle_bind_groups[i] = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{ + .layout = layout, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, sim_param_buffer, 0, sim_params.len * @sizeOf(f32)), + gpu.BindGroup.Entry.buffer(1, particle_buffers[i], 0, initial_particle_data.len * @sizeOf(f32)), + gpu.BindGroup.Entry.buffer(2, particle_buffers[(i + 1) % 2], 0, initial_particle_data.len * @sizeOf(f32)), + }, + })); + } + + app.* = .{ + .timer = try core.Timer.start(), + .title_timer = try core.Timer.start(), + .compute_pipeline = compute_pipeline, + .render_pipeline = render_pipeline, + .sprite_vertex_buffer = sprite_vertex_buffer, + .particle_buffers = particle_buffers, + .particle_bind_groups = particle_bind_groups, + .sim_param_buffer = sim_param_buffer, + .frame_counter = 0, + }; +} + +pub fn deinit(app: *App) void { + defer _ = gpa.deinit(); + defer core.deinit(); + + app.compute_pipeline.release(); + app.render_pipeline.release(); + app.sprite_vertex_buffer.release(); + for (app.particle_buffers) |particle_buffer| particle_buffer.release(); + for (app.particle_bind_groups) |particle_bind_group| particle_bind_group.release(); + app.sim_param_buffer.release(); +} + +pub fn update(app: *App) !bool { + const delta_time = app.timer.lap(); + + var iter = core.pollEvents(); + while (iter.next()) |event| { + if (event == .close) return true; + } + + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = std.mem.zeroes(gpu.Color), + .load_op = .clear, + .store_op = .store, + }; + + const render_pass_descriptor = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{ + color_attachment, + }, + }); + + sim_params[0] = @as(f32, @floatCast(delta_time)); + core.queue.writeBuffer(app.sim_param_buffer, 0, sim_params[0..]); + + const command_encoder = core.device.createCommandEncoder(null); + { + const pass_encoder = command_encoder.beginComputePass(null); + pass_encoder.setPipeline(app.compute_pipeline); + pass_encoder.setBindGroup(0, app.particle_bind_groups[app.frame_counter % 2], null); + pass_encoder.dispatchWorkgroups(@as(u32, @intFromFloat(@ceil(@as(f32, num_particle) / 64))), 1, 1); + pass_encoder.end(); + pass_encoder.release(); + } + { + const pass_encoder = command_encoder.beginRenderPass(&render_pass_descriptor); + pass_encoder.setPipeline(app.render_pipeline); + pass_encoder.setVertexBuffer(0, app.particle_buffers[(app.frame_counter + 1) % 2], 0, num_particle * 4 * @sizeOf(f32)); + pass_encoder.setVertexBuffer(1, app.sprite_vertex_buffer, 0, 6 * @sizeOf(f32)); + pass_encoder.draw(3, num_particle, 0, 0); + pass_encoder.end(); + pass_encoder.release(); + } + + app.frame_counter += 1; + if (app.frame_counter % 60 == 0) { + std.log.info("Frame {}", .{app.frame_counter}); + } + + var command = command_encoder.finish(null); + command_encoder.release(); + core.queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + + core.swap_chain.present(); + back_buffer_view.release(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Boids [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} diff --git a/src/core/examples/boids/sprite.wgsl b/src/core/examples/boids/sprite.wgsl new file mode 100644 index 00000000..c97c5c18 --- /dev/null +++ b/src/core/examples/boids/sprite.wgsl @@ -0,0 +1,15 @@ +@vertex +fn vert_main(@location(0) a_particlePos : vec2, + @location(1) a_particleVel : vec2, + @location(2) a_pos : vec2) -> @builtin(position) vec4 { + let angle = -atan2(a_particleVel.x, a_particleVel.y); + let pos = vec2( + (a_pos.x * cos(angle)) - (a_pos.y * sin(angle)), + (a_pos.x * sin(angle)) + (a_pos.y * cos(angle))); + return vec4(pos + a_particlePos, 0.0, 1.0); +} + +@fragment +fn frag_main() -> @location(0) vec4 { + return vec4(1.0, 1.0, 1.0, 1.0); +} diff --git a/src/core/examples/boids/updateSprites.wgsl b/src/core/examples/boids/updateSprites.wgsl new file mode 100644 index 00000000..89071499 --- /dev/null +++ b/src/core/examples/boids/updateSprites.wgsl @@ -0,0 +1,90 @@ +struct Particle { + pos : vec2, + vel : vec2, +}; +struct SimParams { + deltaT : f32, + rule1Distance : f32, + rule2Distance : f32, + rule3Distance : f32, + rule1Scale : f32, + rule2Scale : f32, + rule3Scale : f32, +}; +struct Particles { + particles : array, +}; +@binding(0) @group(0) var params : SimParams; +@binding(1) @group(0) var particlesA : Particles; +@binding(2) @group(0) var particlesB : Particles; + +// https://github.com/austinEng/Project6-Vulkan-Flocking/blob/master/data/shaders/computeparticles/particle.comp +@compute @workgroup_size(64) +fn main(@builtin(global_invocation_id) GlobalInvocationID : vec3) { + var index : u32 = GlobalInvocationID.x; + + if (index >= arrayLength(&particlesA.particles)) { + return; + } + + var vPos = particlesA.particles[index].pos; + var vVel = particlesA.particles[index].vel; + var cMass = vec2(0.0, 0.0); + var cVel = vec2(0.0, 0.0); + var colVel = vec2(0.0, 0.0); + var cMassCount : u32 = 0u; + var cVelCount : u32 = 0u; + var pos : vec2; + var vel : vec2; + + for (var i : u32 = 0u; i < arrayLength(&particlesA.particles); i = i + 1u) { + if (i == index) { + continue; + } + + pos = particlesA.particles[i].pos.xy; + vel = particlesA.particles[i].vel.xy; + if (distance(pos, vPos) < params.rule1Distance) { + cMass = cMass + pos; + cMassCount = cMassCount + 1u; + } + if (distance(pos, vPos) < params.rule2Distance) { + colVel = colVel - (pos - vPos); + } + if (distance(pos, vPos) < params.rule3Distance) { + cVel = cVel + vel; + cVelCount = cVelCount + 1u; + } + } + if (cMassCount > 0u) { + var temp = f32(cMassCount); + cMass = (cMass / vec2(temp, temp)) - vPos; + } + if (cVelCount > 0u) { + var temp = f32(cVelCount); + cVel = cVel / vec2(temp, temp); + } + vVel = vVel + (cMass * params.rule1Scale) + (colVel * params.rule2Scale) + + (cVel * params.rule3Scale); + + // clamp velocity for a more pleasing simulation + vVel = normalize(vVel) * clamp(length(vVel), 0.0, 0.1); + // kinematic update + vPos = vPos + (vVel * params.deltaT); + // Wrap around boundary + if (vPos.x < -1.0) { + vPos.x = 1.0; + } + if (vPos.x > 1.0) { + vPos.x = -1.0; + } + if (vPos.y < -1.0) { + vPos.y = 1.0; + } + if (vPos.y > 1.0) { + vPos.y = -1.0; + } + // Write back + particlesB.particles[index].pos = vPos; + particlesB.particles[index].vel = vVel; +} diff --git a/src/core/examples/clear-color/main.zig b/src/core/examples/clear-color/main.zig new file mode 100644 index 00000000..7747efd7 --- /dev/null +++ b/src/core/examples/clear-color/main.zig @@ -0,0 +1,78 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const renderer = @import("renderer.zig"); + +pub const App = @This(); + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +title_timer: core.Timer, + +pub fn init(app: *App) !void { + try core.init(.{}); + app.* = .{ + .title_timer = try core.Timer.start(), + }; +} + +pub fn deinit(app: *App) void { + _ = app; + defer _ = gpa.deinit(); + defer core.deinit(); +} + +pub fn update(app: *App) !bool { + var iter = core.pollEvents(); + while (iter.next()) |event| { + switch (event) { + .key_press => |ev| { + if (ev.key == .space) return true; + }, + .close => return true, + else => {}, + } + } + + app.render(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Clear Color [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} + +fn render(app: *App) void { + _ = app; + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = gpu.Color{ .r = 0.0, .g = 0.0, .b = 1.0, .a = 1.0 }, + .load_op = .clear, + .store_op = .store, + }; + + const encoder = core.device.createCommandEncoder(null); + const render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + }); + + const pass = encoder.beginRenderPass(&render_pass_info); + pass.end(); + pass.release(); + + var command = encoder.finish(null); + encoder.release(); + + var queue = core.queue; + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); +} diff --git a/src/core/examples/clear-color/renderer.zig b/src/core/examples/clear-color/renderer.zig new file mode 100644 index 00000000..1ad7924a --- /dev/null +++ b/src/core/examples/clear-color/renderer.zig @@ -0,0 +1,32 @@ +const core = @import("mach").core; +const gpu = core.gpu; + +pub const Renderer = @This(); + +pub fn RenderUpdate() void { + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = gpu.Color{ .r = 0.0, .g = 0.0, .b = 1.0, .a = 1.0 }, + .load_op = .clear, + .store_op = .store, + }; + + const encoder = core.device.createCommandEncoder(null); + const render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + }); + + const pass = encoder.beginRenderPass(&render_pass_info); + pass.end(); + pass.release(); + + var command = encoder.finish(null); + encoder.release(); + + const queue = core.queue; + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); +} diff --git a/src/core/examples/cubemap/cube_mesh.zig b/src/core/examples/cubemap/cube_mesh.zig new file mode 100644 index 00000000..ae5b2912 --- /dev/null +++ b/src/core/examples/cubemap/cube_mesh.zig @@ -0,0 +1,49 @@ +pub const Vertex = extern struct { + pos: @Vector(4, f32), + col: @Vector(4, f32), + uv: @Vector(2, f32), +}; + +pub const vertices = [_]Vertex{ + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 1, 0 } }, + + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 0 } }, + + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } }, + + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } }, + + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } }, + + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } }, +}; diff --git a/src/core/examples/cubemap/main.zig b/src/core/examples/cubemap/main.zig new file mode 100644 index 00000000..1ef2d54c --- /dev/null +++ b/src/core/examples/cubemap/main.zig @@ -0,0 +1,392 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const zm = @import("zmath"); +const zigimg = @import("zigimg"); +const Vertex = @import("cube_mesh.zig").Vertex; +const vertices = @import("cube_mesh.zig").vertices; +const assets = @import("assets"); + +const UniformBufferObject = struct { + mat: zm.Mat, +}; + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +title_timer: core.Timer, +timer: core.Timer, +pipeline: *gpu.RenderPipeline, +vertex_buffer: *gpu.Buffer, +uniform_buffer: *gpu.Buffer, +bind_group: *gpu.BindGroup, +depth_texture: *gpu.Texture, +depth_texture_view: *gpu.TextureView, + +pub const App = @This(); + +pub fn init(app: *App) !void { + try core.init(.{}); + + const allocator = gpa.allocator(); + + const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); + defer shader_module.release(); + + const vertex_attributes = [_]gpu.VertexAttribute{ + .{ .format = .float32x4, .offset = @offsetOf(Vertex, "pos"), .shader_location = 0 }, + .{ .format = .float32x2, .offset = @offsetOf(Vertex, "uv"), .shader_location = 1 }, + }; + + const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{ + .array_stride = @sizeOf(Vertex), + .step_mode = .vertex, + .attributes = &vertex_attributes, + }); + + const blend = gpu.BlendState{ + .color = .{ + .operation = .add, + .src_factor = .src_alpha, + .dst_factor = .one_minus_src_alpha, + }, + .alpha = .{ + .operation = .add, + .src_factor = .one, + .dst_factor = .zero, + }, + }; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + // Enable depth testing so that the fragment closest to the camera + // is rendered in front. + .depth_stencil = &.{ + .format = .depth24_plus, + .depth_write_enabled = .true, + .depth_compare = .less, + }, + .vertex = gpu.VertexState.init(.{ + .module = shader_module, + .entry_point = "vertex_main", + .buffers = &.{vertex_buffer_layout}, + }), + .primitive = .{ + // Since the cube has its face pointing outwards, cull_mode must be + // set to .front or .none here since we are inside the cube looking out. + // Ideally you would set this to .back and have a custom cube primitive + // with the faces pointing towards the inside of the cube. + .cull_mode = .none, + }, + }; + const pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + + const vertex_buffer = core.device.createBuffer(&.{ + .usage = .{ .vertex = true }, + .size = @sizeOf(Vertex) * vertices.len, + .mapped_at_creation = .true, + }); + const vertex_mapped = vertex_buffer.getMappedRange(Vertex, 0, vertices.len); + @memcpy(vertex_mapped.?, vertices[0..]); + vertex_buffer.unmap(); + + const uniform_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = @sizeOf(UniformBufferObject), + .mapped_at_creation = .false, + }); + + // Create a sampler with linear filtering for smooth interpolation. + const sampler = core.device.createSampler(&.{ + .mag_filter = .linear, + .min_filter = .linear, + }); + + const queue = core.queue; + + // WebGPU expects the cubemap textures in this order: (+X,-X,+Y,-Y,+Z,-Z) + var images: [6]zigimg.Image = undefined; + images[0] = try zigimg.Image.fromMemory(allocator, assets.skybox_posx_png); + defer images[0].deinit(); + images[1] = try zigimg.Image.fromMemory(allocator, assets.skybox_negx_png); + defer images[1].deinit(); + images[2] = try zigimg.Image.fromMemory(allocator, assets.skybox_posy_png); + defer images[2].deinit(); + images[3] = try zigimg.Image.fromMemory(allocator, assets.skybox_negy_png); + defer images[3].deinit(); + images[4] = try zigimg.Image.fromMemory(allocator, assets.skybox_posz_png); + defer images[4].deinit(); + images[5] = try zigimg.Image.fromMemory(allocator, assets.skybox_negz_png); + defer images[5].deinit(); + + // Use the first image of the set for sizing + const img_size = gpu.Extent3D{ + .width = @as(u32, @intCast(images[0].width)), + .height = @as(u32, @intCast(images[0].height)), + }; + + // We set depth_or_array_layers to 6 here to indicate there are 6 images in this texture + const tex_size = gpu.Extent3D{ + .width = @as(u32, @intCast(images[0].width)), + .height = @as(u32, @intCast(images[0].height)), + .depth_or_array_layers = 6, + }; + + // Same as a regular texture, but with a Z of 6 (defined in tex_size) + const cube_texture = core.device.createTexture(&.{ + .size = tex_size, + .format = .rgba8_unorm, + .dimension = .dimension_2d, + .usage = .{ + .texture_binding = true, + .copy_dst = true, + .render_attachment = false, + }, + }); + + const data_layout = gpu.Texture.DataLayout{ + .bytes_per_row = @as(u32, @intCast(images[0].width * 4)), + .rows_per_image = @as(u32, @intCast(images[0].height)), + }; + + const encoder = core.device.createCommandEncoder(null); + + // We have to create a staging buffer, copy all the image data into the + // staging buffer at the correct Z offset, encode a command to copy + // the buffer to the texture for each image, then push it to the command + // queue + var staging_buff: [6]*gpu.Buffer = undefined; + var i: u32 = 0; + while (i < 6) : (i += 1) { + staging_buff[i] = core.device.createBuffer(&.{ + .usage = .{ .copy_src = true, .map_write = true }, + .size = @as(u64, @intCast(images[0].width)) * @as(u64, @intCast(images[0].height)) * @sizeOf(u32), + .mapped_at_creation = .true, + }); + switch (images[i].pixels) { + .rgba32 => |pixels| { + // Map a section of the staging buffer + const staging_map = staging_buff[i].getMappedRange(u32, 0, @as(u64, @intCast(images[i].width)) * @as(u64, @intCast(images[i].height))); + // Copy the image data into the mapped buffer + @memcpy(staging_map.?, @as([]u32, @ptrCast(@alignCast(pixels)))); + // And release the mapping + staging_buff[i].unmap(); + }, + .rgb24 => |pixels| { + const staging_map = staging_buff[i].getMappedRange(u32, 0, @as(u64, @intCast(images[i].width)) * @as(u64, @intCast(images[i].height))); + // In this case, we have to convert the data to rgba32 first + const data = try rgb24ToRgba32(allocator, pixels); + defer data.deinit(allocator); + @memcpy(staging_map.?, @as([]u32, @ptrCast(@alignCast(data.rgba32)))); + staging_buff[i].unmap(); + }, + else => @panic("unsupported image color format"), + } + + // These define the source and target for the buffer to texture copy command + const copy_buff = gpu.ImageCopyBuffer{ + .layout = data_layout, + .buffer = staging_buff[i], + }; + const copy_tex = gpu.ImageCopyTexture{ + .texture = cube_texture, + .origin = gpu.Origin3D{ .x = 0, .y = 0, .z = i }, + }; + + // Encode the copy command, we do this for every image in the texture. + encoder.copyBufferToTexture(©_buff, ©_tex, &img_size); + staging_buff[i].release(); + } + // Now that the commands to copy our buffer data to the texture is filled, + // push the encoded commands over to the queue and execute to get the + // texture filled with the image data. + var command = encoder.finish(null); + encoder.release(); + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + + // The textureView in the bind group needs dimension defined as "dimension_cube". + const cube_texture_view = cube_texture.createView(&gpu.TextureView.Descriptor{ .dimension = .dimension_cube }); + cube_texture.release(); + + const bind_group_layout = pipeline.getBindGroupLayout(0); + const bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bind_group_layout, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, uniform_buffer, 0, @sizeOf(UniformBufferObject)), + gpu.BindGroup.Entry.sampler(1, sampler), + gpu.BindGroup.Entry.textureView(2, cube_texture_view), + }, + }), + ); + sampler.release(); + cube_texture_view.release(); + bind_group_layout.release(); + + const depth_texture = core.device.createTexture(&gpu.Texture.Descriptor{ + .size = gpu.Extent3D{ + .width = core.descriptor.width, + .height = core.descriptor.height, + }, + .format = .depth24_plus, + .usage = .{ + .render_attachment = true, + .texture_binding = true, + }, + }); + + const depth_texture_view = depth_texture.createView(&gpu.TextureView.Descriptor{ + .format = .depth24_plus, + .dimension = .dimension_2d, + .array_layer_count = 1, + .mip_level_count = 1, + }); + + app.timer = try core.Timer.start(); + app.title_timer = try core.Timer.start(); + app.pipeline = pipeline; + app.vertex_buffer = vertex_buffer; + app.uniform_buffer = uniform_buffer; + app.bind_group = bind_group; + app.depth_texture = depth_texture; + app.depth_texture_view = depth_texture_view; +} + +pub fn deinit(app: *App) void { + defer _ = gpa.deinit(); + defer core.deinit(); + + app.pipeline.release(); + app.vertex_buffer.release(); + app.uniform_buffer.release(); + app.bind_group.release(); + app.depth_texture.release(); + app.depth_texture_view.release(); +} + +pub fn update(app: *App) !bool { + var iter = core.pollEvents(); + while (iter.next()) |event| { + switch (event) { + .key_press => |ev| { + if (ev.key == .space) return true; + }, + .close => return true, + .framebuffer_resize => |ev| { + // If window is resized, recreate depth buffer otherwise we cannot use it. + app.depth_texture.release(); + app.depth_texture = core.device.createTexture(&gpu.Texture.Descriptor{ + .size = gpu.Extent3D{ + .width = ev.width, + .height = ev.height, + }, + .format = .depth24_plus, + .usage = .{ + .render_attachment = true, + .texture_binding = true, + }, + }); + app.depth_texture_view.release(); + app.depth_texture_view = app.depth_texture.createView(&gpu.TextureView.Descriptor{ + .format = .depth24_plus, + .dimension = .dimension_2d, + .array_layer_count = 1, + .mip_level_count = 1, + }); + }, + else => {}, + } + } + + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = .{ .r = 0.5, .g = 0.5, .b = 0.5, .a = 0.0 }, + .load_op = .clear, + .store_op = .store, + }; + + const encoder = core.device.createCommandEncoder(null); + const render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + .depth_stencil_attachment = &.{ + .view = app.depth_texture_view, + .depth_clear_value = 1.0, + .depth_load_op = .clear, + .depth_store_op = .store, + }, + }); + + { + const time = app.timer.read(); + const aspect = @as(f32, @floatFromInt(core.descriptor.width)) / @as(f32, @floatFromInt(core.descriptor.height)); + const proj = zm.perspectiveFovRh((2 * std.math.pi) / 5.0, aspect, 0.1, 3000); + const model = zm.mul( + zm.scaling(1000, 1000, 1000), + zm.rotationX(std.math.pi / 2.0 * 3.0), + ); + const view = zm.mul( + zm.mul( + zm.lookAtRh( + zm.Vec{ 0, 0, 0, 1 }, + zm.Vec{ 1, 0, 0, 1 }, + zm.Vec{ 0, 0, 1, 0 }, + ), + zm.rotationY(time * 0.2), + ), + zm.rotationX((std.math.pi / 10.0) * std.math.sin(time)), + ); + + const mvp = zm.mul(zm.mul(zm.transpose(model), view), proj); + const ubo = UniformBufferObject{ .mat = mvp }; + + encoder.writeBuffer(app.uniform_buffer, 0, &[_]UniformBufferObject{ubo}); + } + + const pass = encoder.beginRenderPass(&render_pass_info); + pass.setPipeline(app.pipeline); + pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len); + pass.setBindGroup(0, app.bind_group, &.{}); + pass.draw(vertices.len, 1, 0, 0); + pass.end(); + pass.release(); + + var command = encoder.finish(null); + encoder.release(); + + const queue = core.queue; + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Cube Map [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} + +fn rgb24ToRgba32(allocator: std.mem.Allocator, in: []zigimg.color.Rgb24) !zigimg.color.PixelStorage { + const out = try zigimg.color.PixelStorage.init(allocator, .rgba32, in.len); + var i: usize = 0; + while (i < in.len) : (i += 1) { + out.rgba32[i] = zigimg.color.Rgba32{ .r = in[i].r, .g = in[i].g, .b = in[i].b, .a = 255 }; + } + return out; +} diff --git a/src/core/examples/cubemap/shader.wgsl b/src/core/examples/cubemap/shader.wgsl new file mode 100644 index 00000000..1442990c --- /dev/null +++ b/src/core/examples/cubemap/shader.wgsl @@ -0,0 +1,34 @@ +struct Uniforms { + modelViewProjectionMatrix : mat4x4, +} +@binding(0) @group(0) var uniforms : Uniforms; + +struct VertexOutput { + @builtin(position) Position : vec4, + @location(0) fragUV : vec2, + @location(1) fragPosition: vec4, +} + +@vertex +fn vertex_main( + @location(0) position : vec4, + @location(1) uv : vec2 +) -> VertexOutput { + var output : VertexOutput; + output.Position = uniforms.modelViewProjectionMatrix * position; + output.fragUV = uv; + output.fragPosition = 0.5 * (position + vec4(1.0, 1.0, 1.0, 1.0)); + return output; +} + +@group(0) @binding(1) var mySampler: sampler; +@group(0) @binding(2) var myTexture: texture_cube; + +@fragment +fn frag_main( + @location(0) fragUV: vec2, + @location(1) fragPosition: vec4 +) -> @location(0) vec4 { + var cubemapVec = fragPosition.xyz - vec3(0.5, 0.5, 0.5); + return textureSample(myTexture, mySampler, cubemapVec); +} diff --git a/src/core/examples/deferred-rendering/fragmentDeferredRendering.wgsl b/src/core/examples/deferred-rendering/fragmentDeferredRendering.wgsl new file mode 100644 index 00000000..71ed702e --- /dev/null +++ b/src/core/examples/deferred-rendering/fragmentDeferredRendering.wgsl @@ -0,0 +1,83 @@ + +@group(0) @binding(0) var gBufferNormal: texture_2d; +@group(0) @binding(1) var gBufferAlbedo: texture_2d; +@group(0) @binding(2) var gBufferDepth: texture_depth_2d; + +struct LightData { + position : vec4, + color : vec3, + radius : f32, +} +struct LightsBuffer { + lights: array, +} +@group(1) @binding(0) var lightsBuffer: LightsBuffer; + +struct Config { + numLights : u32, +} +struct Camera { + viewProjectionMatrix : mat4x4, + invViewProjectionMatrix : mat4x4, +} +@group(1) @binding(1) var config: Config; +@group(1) @binding(2) var camera: Camera; + +fn world_from_screen_coord(coord : vec2, depth_sample: f32) -> vec3 { + // reconstruct world-space position from the screen coordinate. + let posClip = vec4(coord.x * 2.0 - 1.0, (1.0 - coord.y) * 2.0 - 1.0, depth_sample, 1.0); + let posWorldW = camera.invViewProjectionMatrix * posClip; + let posWorld = posWorldW.xyz / posWorldW.www; + return posWorld; +} + +@fragment +fn main( + @builtin(position) coord : vec4 +) -> @location(0) vec4 { + var result : vec3; + + let depth = textureLoad( + gBufferDepth, + vec2(floor(coord.xy)), + 0 + ); + + // Don't light the sky. + if (depth >= 1.0) { + discard; + } + + let bufferSize = textureDimensions(gBufferDepth); + let coordUV = coord.xy / vec2(bufferSize); + let position = world_from_screen_coord(coordUV, depth); + + let normal = textureLoad( + gBufferNormal, + vec2(floor(coord.xy)), + 0 + ).xyz; + + let albedo = textureLoad( + gBufferAlbedo, + vec2(floor(coord.xy)), + 0 + ).rgb; + + for (var i = 0u; i < config.numLights; i++) { + let L = lightsBuffer.lights[i].position.xyz - position; + let distance = length(L); + if (distance > lightsBuffer.lights[i].radius) { + continue; + } + let lambert = max(dot(normal, normalize(L)), 0.0); + result += vec3( + lambert * pow(1.0 - distance / lightsBuffer.lights[i].radius, 2.0) * lightsBuffer.lights[i].color * albedo + ); + } + + // some manual ambient + result += vec3(0.2); + + return vec4(result, 1.0); +} diff --git a/src/core/examples/deferred-rendering/fragmentGBuffersDebugView.wgsl b/src/core/examples/deferred-rendering/fragmentGBuffersDebugView.wgsl new file mode 100644 index 00000000..cedf7645 --- /dev/null +++ b/src/core/examples/deferred-rendering/fragmentGBuffersDebugView.wgsl @@ -0,0 +1,44 @@ + +@group(0) @binding(0) var gBufferNormal: texture_2d; +@group(0) @binding(1) var gBufferAlbedo: texture_2d; +@group(0) @binding(2) var gBufferDepth: texture_depth_2d; + +@group(1) @binding(0) var canvas : CanvasConstants; + +struct CanvasConstants { + size: vec2, +} + +@fragment +fn main( + @builtin(position) coord : vec4 +) -> @location(0) vec4 { + var result : vec4; + let c = coord.xy / vec2(canvas.size.x, canvas.size.y); + if (c.x < 0.33333) { + let rawDepth = textureLoad( + gBufferDepth, + vec2(floor(coord.xy)), + 0 + ); + // remap depth into something a bit more visible + let depth = (1.0 - rawDepth) * 50.0; + result = vec4(depth); + } else if (c.x < 0.66667) { + result = textureLoad( + gBufferNormal, + vec2(floor(coord.xy)), + 0 + ); + result.x = (result.x + 1.0) * 0.5; + result.y = (result.y + 1.0) * 0.5; + result.z = (result.z + 1.0) * 0.5; + } else { + result = textureLoad( + gBufferAlbedo, + vec2(floor(coord.xy)), + 0 + ); + } + return result; +} diff --git a/src/core/examples/deferred-rendering/fragmentWriteGBuffers.wgsl b/src/core/examples/deferred-rendering/fragmentWriteGBuffers.wgsl new file mode 100644 index 00000000..3cd2f652 --- /dev/null +++ b/src/core/examples/deferred-rendering/fragmentWriteGBuffers.wgsl @@ -0,0 +1,22 @@ +struct GBufferOutput { + @location(0) normal : vec4, + + // Textures: diffuse color, specular color, smoothness, emissive etc. could go here + @location(1) albedo : vec4, +} + +@fragment +fn main( + @location(0) fragNormal: vec3, + @location(1) fragUV : vec2 +) -> GBufferOutput { + // faking some kind of checkerboard texture + let uv = floor(30.0 * fragUV); + let c = 0.2 + 0.5 * ((uv.x + uv.y) - 2.0 * floor((uv.x + uv.y) / 2.0)); + + var output : GBufferOutput; + output.normal = vec4(fragNormal, 1.0); + output.albedo = vec4(c, c, c, 1.0); + + return output; +} diff --git a/src/core/examples/deferred-rendering/lightUpdate.wgsl b/src/core/examples/deferred-rendering/lightUpdate.wgsl new file mode 100644 index 00000000..2c7672dd --- /dev/null +++ b/src/core/examples/deferred-rendering/lightUpdate.wgsl @@ -0,0 +1,34 @@ +struct LightData { + position : vec4, + color : vec3, + radius : f32, +} +struct LightsBuffer { + lights: array, +} +@group(0) @binding(0) var lightsBuffer: LightsBuffer; + +struct Config { + numLights : u32, +} +@group(0) @binding(1) var config: Config; + +struct LightExtent { + min : vec4, + max : vec4, +} +@group(0) @binding(2) var lightExtent: LightExtent; + +@compute @workgroup_size(64, 1, 1) +fn main(@builtin(global_invocation_id) GlobalInvocationID : vec3) { + var index = GlobalInvocationID.x; + if (index >= config.numLights) { + return; + } + + lightsBuffer.lights[index].position.y = lightsBuffer.lights[index].position.y - 0.5 - 0.003 * (f32(index) - 64.0 * floor(f32(index) / 64.0)); + + if (lightsBuffer.lights[index].position.y < lightExtent.min.y) { + lightsBuffer.lights[index].position.y = lightExtent.max.y; + } +} diff --git a/src/core/examples/deferred-rendering/main.zig b/src/core/examples/deferred-rendering/main.zig new file mode 100644 index 00000000..80070e19 --- /dev/null +++ b/src/core/examples/deferred-rendering/main.zig @@ -0,0 +1,1210 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const m3d = @import("model3d"); +const zm = @import("zmath"); +const assets = @import("assets"); +const VertexWriter = @import("vertex_writer.zig").VertexWriter; + +pub const App = @This(); + +const Vec2 = [2]f32; +const Vec3 = [3]f32; +const Vec4 = [4]f32; +const Mat4 = [4]Vec4; + +fn Dimensions2D(comptime T: type) type { + return struct { + width: T, + height: T, + }; +} + +const Vertex = extern struct { + position: Vec3, + normal: Vec3, + uv: Vec2, +}; + +const ViewMatrices = struct { + up_vector: zm.Vec, + origin: zm.Vec, + projection_matrix: zm.Mat, + view_proj_matrix: zm.Mat, +}; + +const TextureQuadPass = struct { + color_attachment: gpu.RenderPassColorAttachment, + descriptor: gpu.RenderPassDescriptor, +}; + +const WriteGBufferPass = struct { + color_attachments: [2]gpu.RenderPassColorAttachment, + depth_stencil_attachment: gpu.RenderPassDepthStencilAttachment, + descriptor: gpu.RenderPassDescriptor, +}; + +const RenderMode = enum(u32) { + rendering, + gbuffer_view, +}; + +const Settings = struct { + render_mode: RenderMode, + lights_count: i32, +}; + +// +// Constants +// + +const max_num_lights = 1024; +const light_data_stride = 8; +const light_extent_min = Vec3{ -50.0, -30.0, -50.0 }; +const light_extent_max = Vec3{ 50.0, 30.0, 50.0 }; +const camera_uniform_buffer_size = @sizeOf(Mat4) * 2; + +// +// Member variables +// + +const GBuffer = struct { + texture_2d_float16: *gpu.Texture, + texture_albedo: *gpu.Texture, + texture_depth: *gpu.Texture, + texture_views: [3]*gpu.TextureView, +}; + +const Lights = struct { + buffer: *gpu.Buffer, + buffer_size: u64, + extent_buffer: *gpu.Buffer, + extent_buffer_size: u64, + config_uniform_buffer: *gpu.Buffer, + config_uniform_buffer_size: u64, + buffer_bind_group: *gpu.BindGroup, + buffer_bind_group_layout: *gpu.BindGroupLayout, + buffer_compute_bind_group: *gpu.BindGroup, + buffer_compute_bind_group_layout: *gpu.BindGroupLayout, +}; + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +title_timer: core.Timer, +timer: core.Timer, +delta_time: f32, + +camera_rotation: f32, +vertex_buffer: *gpu.Buffer, +vertex_count: u32, +index_buffer: *gpu.Buffer, +index_count: u32, +gbuffer: GBuffer, +model_uniform_buffer: *gpu.Buffer, +camera_uniform_buffer: *gpu.Buffer, +surface_size_uniform_buffer: *gpu.Buffer, +lights: Lights, +view_matrices: ViewMatrices, + +// Bind groups +scene_uniform_bind_group: *gpu.BindGroup, +surface_size_uniform_bind_group: *gpu.BindGroup, +gbuffer_textures_bind_group: *gpu.BindGroup, + +// Bind group layouts +scene_uniform_bind_group_layout: *gpu.BindGroupLayout, +surface_size_uniform_bind_group_layout: *gpu.BindGroupLayout, +gbuffer_textures_bind_group_layout: *gpu.BindGroupLayout, + +// Pipelines +write_gbuffers_pipeline: *gpu.RenderPipeline, +gbuffers_debug_view_pipeline: *gpu.RenderPipeline, +deferred_render_pipeline: *gpu.RenderPipeline, +light_update_compute_pipeline: *gpu.ComputePipeline, + +// Pipeline layouts +write_gbuffers_pipeline_layout: *gpu.PipelineLayout, +gbuffers_debug_view_pipeline_layout: *gpu.PipelineLayout, +deferred_render_pipeline_layout: *gpu.PipelineLayout, +light_update_compute_pipeline_layout: *gpu.PipelineLayout, + +// Render pass descriptor +write_gbuffer_pass: WriteGBufferPass, +texture_quad_pass: TextureQuadPass, +settings: Settings, + +screen_dimensions: Dimensions2D(u32), +is_paused: bool, + +// +// Functions +// + +pub fn init(app: *App) !void { + try core.init(.{}); + + // This example has some frame-rate-dependent animation, so restrict frame rate to 60hz. + core.setFrameRateLimit(60); + + app.timer = try core.Timer.start(); + app.title_timer = try core.Timer.start(); + + app.camera_rotation = 0.0; + app.is_paused = false; + app.settings.render_mode = .rendering; + app.settings.lights_count = 128; + + app.screen_dimensions = Dimensions2D(u32){ + .width = core.descriptor.width, + .height = core.descriptor.height, + }; + + try app.loadMeshFromModel3d(std.heap.c_allocator, assets.stanford_dragon_m3d); + app.prepareGBufferTextureRenderTargets(); + app.prepareBindGroupLayouts(); + app.prepareRenderPipelineLayouts(); + app.prepareWriteGBuffersPipeline(); + app.prepareGBuffersDebugViewPipeline(); + app.prepareDeferredRenderPipeline(); + app.setupRenderPasses(); + app.prepareUniformBuffers(); + app.prepareComputePipelineLayout(); + app.prepareLightUpdateComputePipeline(); + app.prepareLights(); + app.prepareViewMatrices(); + app.printControls(); +} + +pub fn deinit(app: *App) void { + defer _ = gpa.deinit(); + defer core.deinit(); + + app.write_gbuffers_pipeline.release(); + app.gbuffers_debug_view_pipeline.release(); + app.deferred_render_pipeline.release(); + app.light_update_compute_pipeline.release(); + + app.write_gbuffers_pipeline_layout.release(); + app.gbuffers_debug_view_pipeline_layout.release(); + app.deferred_render_pipeline_layout.release(); + app.light_update_compute_pipeline_layout.release(); + + app.scene_uniform_bind_group.release(); + app.surface_size_uniform_bind_group.release(); + app.gbuffer_textures_bind_group.release(); + + app.lights.buffer.release(); + app.lights.extent_buffer.release(); + app.lights.config_uniform_buffer.release(); + app.lights.buffer_bind_group.release(); + app.lights.buffer_bind_group_layout.release(); + app.lights.buffer_compute_bind_group.release(); + app.lights.buffer_compute_bind_group_layout.release(); + + app.gbuffer.texture_views[0].release(); + app.gbuffer.texture_views[1].release(); + app.gbuffer.texture_views[2].release(); + + app.gbuffer.texture_2d_float16.release(); + app.gbuffer.texture_albedo.release(); + app.gbuffer.texture_depth.release(); + + app.scene_uniform_bind_group_layout.release(); + app.surface_size_uniform_bind_group_layout.release(); + app.gbuffer_textures_bind_group_layout.release(); + + app.surface_size_uniform_buffer.release(); + app.model_uniform_buffer.release(); + app.camera_uniform_buffer.release(); + app.vertex_buffer.release(); + app.index_buffer.release(); +} + +pub fn update(app: *App) !bool { + app.delta_time = app.timer.lap(); + + var iter = core.pollEvents(); + while (iter.next()) |event| { + app.updateUI(event); + switch (event) { + .framebuffer_resize => |ev| { + app.screen_dimensions.width = ev.width; + app.screen_dimensions.height = ev.height; + + // TODO: we use destroy() here instead of release() because our reference counting + // is wrong somewhere else. + app.gbuffer.texture_2d_float16.release(); + app.gbuffer.texture_albedo.release(); + app.gbuffer.texture_depth.release(); + app.gbuffer.texture_views[0].release(); + app.gbuffer.texture_views[1].release(); + app.gbuffer.texture_views[2].release(); + app.gbuffer_textures_bind_group.release(); + + app.prepareGBufferTextureRenderTargets(); + app.setupRenderPasses(); + + const bind_group_entries = [_]gpu.BindGroup.Entry{ + gpu.BindGroup.Entry.textureView(0, app.gbuffer.texture_views[0]), + gpu.BindGroup.Entry.textureView(1, app.gbuffer.texture_views[1]), + gpu.BindGroup.Entry.textureView(2, app.gbuffer.texture_views[2]), + }; + app.gbuffer_textures_bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = app.gbuffer_textures_bind_group_layout, + .entries = &bind_group_entries, + }), + ); + + app.prepareViewMatrices(); + }, + .close => return true, + else => {}, + } + } + + if (!app.is_paused) { + app.updateUniformBuffers(); + } + + const command = try app.buildCommandBuffer(); + const queue = core.queue; + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + core.swap_chain.getCurrentTextureView().?.release(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Deferred Rendering [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} + +fn loadMeshFromModel3d(app: *App, allocator: std.mem.Allocator, model_data: [:0]const u8) !void { + const m3d_model = m3d.load(model_data, null, null, null) orelse return error.LoadModelFailed; + + const vertex_count = m3d_model.handle.numvertex; + const vertices = m3d_model.handle.vertex[0..vertex_count]; + + const face_count = m3d_model.handle.numface; + app.index_count = (face_count * 3) + 6; + + var vertex_writer = try VertexWriter(Vertex, u16).init( + allocator, + @as(u16, @intCast(app.index_count)), + @as(u16, @intCast(vertex_count)), + @as(u16, @intCast(face_count * 3)), + ); + defer vertex_writer.deinit(allocator); + + const scale: f32 = 80.0; + const plane_xy = [2]usize{ 0, 1 }; + var extent_min = [2]f32{ std.math.floatMax(f32), std.math.floatMax(f32) }; + var extent_max = [2]f32{ std.math.floatMin(f32), std.math.floatMin(f32) }; + + var i: usize = 0; + while (i < face_count) : (i += 1) { + const face = m3d_model.handle.face[i]; + var x: usize = 0; + while (x < 3) : (x += 1) { + const vertex_index = face.vertex[x]; + const normal_index = face.normal[x]; + const position = Vec3{ + vertices[vertex_index].x * scale, + vertices[vertex_index].y * scale, + vertices[vertex_index].z * scale, + }; + extent_min[0] = @min(position[plane_xy[0]], extent_min[0]); + extent_min[1] = @min(position[plane_xy[1]], extent_min[1]); + extent_max[0] = @max(position[plane_xy[0]], extent_max[0]); + extent_max[1] = @max(position[plane_xy[1]], extent_max[1]); + const vertex = Vertex{ .position = position, .normal = .{ + vertices[normal_index].x, + vertices[normal_index].y, + vertices[normal_index].z, + }, .uv = .{ position[plane_xy[0]], position[plane_xy[1]] } }; + vertex_writer.put(vertex, @as(u16, @intCast(vertex_index))); + } + } + + const vertex_buffer = vertex_writer.vertices[0 .. vertex_writer.next_packed_index + 4]; + const index_buffer = vertex_writer.indices; + + app.vertex_count = @as(u32, @intCast(vertex_buffer.len)); + + // + // Compute UV values + // + for (vertex_buffer) |*vertex| { + vertex.uv = .{ + (vertex.uv[0] - extent_min[0]) / (extent_max[0] - extent_min[0]), + (vertex.uv[1] - extent_min[1]) / (extent_max[1] - extent_min[1]), + }; + } + + // + // Manually append ground plane to mesh + // + { + const last_vertex_index: u16 = @as(u16, @intCast(vertex_buffer.len - 4)); + const index_base = index_buffer.len - 6; + index_buffer[index_base + 0] = last_vertex_index; + index_buffer[index_base + 1] = last_vertex_index + 2; + index_buffer[index_base + 2] = last_vertex_index + 1; + index_buffer[index_base + 3] = last_vertex_index; + index_buffer[index_base + 4] = last_vertex_index + 1; + index_buffer[index_base + 5] = last_vertex_index + 3; + } + + { + const index_base = vertex_buffer.len - 4; + vertex_buffer[index_base + 0].position = .{ -100.0, 20.0, -100.0 }; + vertex_buffer[index_base + 1].position = .{ 100.0, 20.0, 100.0 }; + vertex_buffer[index_base + 2].position = .{ -100.0, 20.0, 100.0 }; + vertex_buffer[index_base + 3].position = .{ 100.0, 20.0, -100.0 }; + vertex_buffer[index_base + 0].normal = .{ 0.0, 1.0, 0.0 }; + vertex_buffer[index_base + 1].normal = .{ 0.0, 1.0, 0.0 }; + vertex_buffer[index_base + 2].normal = .{ 0.0, 1.0, 0.0 }; + vertex_buffer[index_base + 3].normal = .{ 0.0, 1.0, 0.0 }; + vertex_buffer[index_base + 0].uv = .{ 0.0, 0.0 }; + vertex_buffer[index_base + 1].uv = .{ 1.0, 1.0 }; + vertex_buffer[index_base + 2].uv = .{ 0.0, 1.0 }; + vertex_buffer[index_base + 3].uv = .{ 1.0, 0.0 }; + } + + { + const buffer_size = vertex_buffer.len * @sizeOf(Vertex); + app.vertex_buffer = core.device.createBuffer(&.{ + .usage = .{ .vertex = true }, + .size = roundToMultipleOf4(u64, buffer_size), + .mapped_at_creation = .true, + }); + var mapping = app.vertex_buffer.getMappedRange(Vertex, 0, vertex_buffer.len).?; + @memcpy(mapping[0..vertex_buffer.len], vertex_buffer); + app.vertex_buffer.unmap(); + } + { + const buffer_size = index_buffer.len * @sizeOf(u16); + app.index_buffer = core.device.createBuffer(&.{ + .usage = .{ .index = true }, + .size = roundToMultipleOf4(u64, buffer_size), + .mapped_at_creation = .true, + }); + var mapping = app.index_buffer.getMappedRange(u16, 0, index_buffer.len).?; + @memcpy(mapping[0..index_buffer.len], index_buffer); + app.index_buffer.unmap(); + } +} + +fn prepareGBufferTextureRenderTargets(app: *App) void { + var screen_extent = gpu.Extent3D{ + .width = app.screen_dimensions.width, + .height = app.screen_dimensions.height, + .depth_or_array_layers = 2, + }; + screen_extent.depth_or_array_layers = 1; + app.gbuffer.texture_2d_float16 = core.device.createTexture(&.{ + .size = screen_extent, + .format = .rgba16_float, + .mip_level_count = 1, + .sample_count = 1, + .usage = .{ + .texture_binding = true, + .render_attachment = true, + }, + }); + app.gbuffer.texture_albedo = core.device.createTexture(&.{ + .size = screen_extent, + .format = .bgra8_unorm, + .usage = .{ + .texture_binding = true, + .render_attachment = true, + }, + }); + app.gbuffer.texture_depth = core.device.createTexture(&.{ + .size = screen_extent, + .mip_level_count = 1, + .sample_count = 1, + .dimension = .dimension_2d, + .format = .depth24_plus, + .usage = .{ + .texture_binding = true, + .render_attachment = true, + }, + }); + + var texture_view_descriptor = gpu.TextureView.Descriptor{ + .format = .undefined, + .dimension = .dimension_2d, + .array_layer_count = 1, + .aspect = .all, + .base_array_layer = 0, + }; + + texture_view_descriptor.format = .rgba16_float; + app.gbuffer.texture_views[0] = app.gbuffer.texture_2d_float16.createView(&texture_view_descriptor); + + texture_view_descriptor.format = .bgra8_unorm; + app.gbuffer.texture_views[1] = app.gbuffer.texture_albedo.createView(&texture_view_descriptor); + + texture_view_descriptor.format = .depth24_plus; + app.gbuffer.texture_views[2] = app.gbuffer.texture_depth.createView(&texture_view_descriptor); +} + +fn prepareBindGroupLayouts(app: *App) void { + { + const bind_group_layout_entries = [_]gpu.BindGroupLayout.Entry{ + gpu.BindGroupLayout.Entry.texture(0, .{ .fragment = true }, .unfilterable_float, .dimension_2d, false), + gpu.BindGroupLayout.Entry.texture(1, .{ .fragment = true }, .unfilterable_float, .dimension_2d, false), + gpu.BindGroupLayout.Entry.texture(2, .{ .fragment = true }, .depth, .dimension_2d, false), + }; + app.gbuffer_textures_bind_group_layout = core.device.createBindGroupLayout( + &gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &bind_group_layout_entries, + }), + ); + } + { + const min_binding_size = light_data_stride * max_num_lights * @sizeOf(f32); + const visibility = gpu.ShaderStageFlags{ .fragment = true, .compute = true }; + const bind_group_layout_entries = [_]gpu.BindGroupLayout.Entry{ + gpu.BindGroupLayout.Entry.buffer( + 0, + visibility, + .read_only_storage, + false, + min_binding_size, + ), + gpu.BindGroupLayout.Entry.buffer(1, visibility, .uniform, false, @sizeOf(u32)), + gpu.BindGroupLayout.Entry.buffer(2, .{ .fragment = true }, .uniform, false, @sizeOf(Mat4) * 2), + }; + app.lights.buffer_bind_group_layout = core.device.createBindGroupLayout( + &gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &bind_group_layout_entries, + }), + ); + } + { + const bind_group_layout_entries = [_]gpu.BindGroupLayout.Entry{ + gpu.BindGroupLayout.Entry.buffer(0, .{ .fragment = true }, .uniform, false, @sizeOf(Vec2)), + }; + app.surface_size_uniform_bind_group_layout = core.device.createBindGroupLayout( + &gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &bind_group_layout_entries, + }), + ); + } + { + const bind_group_layout_entries = [_]gpu.BindGroupLayout.Entry{ + gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, false, @sizeOf(Mat4) * 2), + gpu.BindGroupLayout.Entry.buffer(1, .{ .vertex = true }, .uniform, false, @sizeOf(Mat4) * 2), + }; + app.scene_uniform_bind_group_layout = core.device.createBindGroupLayout( + &gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &bind_group_layout_entries, + }), + ); + } + { + const bind_group_layout_entries = [_]gpu.BindGroupLayout.Entry{ + gpu.BindGroupLayout.Entry.buffer(0, .{ .compute = true }, .storage, false, @sizeOf(f32) * light_data_stride * max_num_lights), + gpu.BindGroupLayout.Entry.buffer(1, .{ .compute = true }, .uniform, false, @sizeOf(u32)), + gpu.BindGroupLayout.Entry.buffer(2, .{ .compute = true }, .uniform, false, camera_uniform_buffer_size), + }; + app.lights.buffer_compute_bind_group_layout = core.device.createBindGroupLayout( + &gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &bind_group_layout_entries, + }), + ); + } +} + +fn prepareRenderPipelineLayouts(app: *App) void { + { + // Write GBuffers pipeline layout + const bind_group_layouts = [_]*gpu.BindGroupLayout{app.scene_uniform_bind_group_layout}; + app.write_gbuffers_pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &bind_group_layouts, + })); + } + { + // GBuffers debug view pipeline layout + const bind_group_layouts = [_]*gpu.BindGroupLayout{ + app.gbuffer_textures_bind_group_layout, + app.surface_size_uniform_bind_group_layout, + }; + app.gbuffers_debug_view_pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &bind_group_layouts, + })); + } + { + // Deferred render pipeline layout + const bind_group_layouts = [_]*gpu.BindGroupLayout{ + app.gbuffer_textures_bind_group_layout, + app.lights.buffer_bind_group_layout, + app.surface_size_uniform_bind_group_layout, + }; + app.deferred_render_pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &bind_group_layouts, + })); + } +} + +fn prepareWriteGBuffersPipeline(app: *App) void { + const color_target_states = [_]gpu.ColorTargetState{ + .{ .format = .rgba16_float }, + .{ .format = .bgra8_unorm }, + }; + + const write_gbuffers_vertex_buffer_layout = gpu.VertexBufferLayout.init(.{ + .array_stride = @sizeOf(Vertex), + .step_mode = .vertex, + .attributes = &.{ + .{ .format = .float32x3, .offset = @offsetOf(Vertex, "position"), .shader_location = 0 }, + .{ .format = .float32x3, .offset = @offsetOf(Vertex, "normal"), .shader_location = 1 }, + .{ .format = .float32x2, .offset = @offsetOf(Vertex, "uv"), .shader_location = 2 }, + }, + }); + + const vertex_shader_module = core.device.createShaderModuleWGSL( + "vertexWriteGBuffers.wgsl", + @embedFile("vertexWriteGBuffers.wgsl"), + ); + const fragment_shader_module = core.device.createShaderModuleWGSL( + "fragmentWriteGBuffers.wgsl", + @embedFile("fragmentWriteGBuffers.wgsl"), + ); + + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .label = "gbuffers_pipeline", + .layout = app.write_gbuffers_pipeline_layout, + .primitive = .{ .cull_mode = .back }, + .depth_stencil = &.{ + .format = .depth24_plus, + .depth_write_enabled = .true, + .depth_compare = .less, + }, + .vertex = gpu.VertexState.init(.{ + .module = vertex_shader_module, + .entry_point = "main", + .buffers = &.{write_gbuffers_vertex_buffer_layout}, + }), + .fragment = &gpu.FragmentState.init(.{ + .module = fragment_shader_module, + .entry_point = "main", + .targets = &color_target_states, + }), + }; + app.write_gbuffers_pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + + vertex_shader_module.release(); + fragment_shader_module.release(); +} + +fn prepareGBuffersDebugViewPipeline(app: *App) void { + const blend_component_descriptor = gpu.BlendComponent{ + .operation = .add, + .src_factor = .one, + .dst_factor = .zero, + }; + + const color_target_state = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &.{ + .color = blend_component_descriptor, + .alpha = blend_component_descriptor, + }, + }; + + const vertex_shader_module = core.device.createShaderModuleWGSL( + "vertexTextureQuad.wgsl", + @embedFile("vertexTextureQuad.wgsl"), + ); + const fragment_shader_module = core.device.createShaderModuleWGSL( + "fragmentGBuffersDebugView.wgsl", + @embedFile("fragmentGBuffersDebugView.wgsl"), + ); + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .layout = app.gbuffers_debug_view_pipeline_layout, + .primitive = .{ + .cull_mode = .back, + }, + .vertex = gpu.VertexState.init(.{ + .module = vertex_shader_module, + .entry_point = "main", + }), + .fragment = &gpu.FragmentState.init(.{ + .module = fragment_shader_module, + .entry_point = "main", + .targets = &.{color_target_state}, + }), + }; + app.gbuffers_debug_view_pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + vertex_shader_module.release(); + fragment_shader_module.release(); +} + +fn prepareDeferredRenderPipeline(app: *App) void { + const blend_component_descriptor = gpu.BlendComponent{ + .operation = .add, + .src_factor = .one, + .dst_factor = .zero, + }; + + const color_target_state = gpu.ColorTargetState{ + .format = .bgra8_unorm, + .blend = &.{ + .color = blend_component_descriptor, + .alpha = blend_component_descriptor, + }, + }; + + const vertex_shader_module = core.device.createShaderModuleWGSL( + "vertexTextureQuad.wgsl", + @embedFile("vertexTextureQuad.wgsl"), + ); + const fragment_shader_module = core.device.createShaderModuleWGSL( + "fragmentDeferredRendering.wgsl", + @embedFile("fragmentDeferredRendering.wgsl"), + ); + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .layout = app.deferred_render_pipeline_layout, + .primitive = .{ + .cull_mode = .back, + }, + .vertex = gpu.VertexState.init(.{ + .module = vertex_shader_module, + .entry_point = "main", + }), + .fragment = &gpu.FragmentState.init(.{ + .module = fragment_shader_module, + .entry_point = "main", + .targets = &.{color_target_state}, + }), + }; + app.deferred_render_pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + vertex_shader_module.release(); + fragment_shader_module.release(); +} + +fn setupRenderPasses(app: *App) void { + { + // Write GBuffer pass + app.write_gbuffer_pass.color_attachments = [_]gpu.RenderPassColorAttachment{ + .{ + .view = app.gbuffer.texture_views[0], + .load_op = .clear, + .store_op = .store, + .clear_value = .{ + .r = 0.0, + .g = 0.0, + .b = 1.0, + .a = 1.0, + }, + }, + .{ + .view = app.gbuffer.texture_views[1], + .load_op = .clear, + .store_op = .store, + .clear_value = .{ + .r = 0.0, + .g = 0.0, + .b = 0.0, + .a = 1.0, + }, + }, + }; + + app.write_gbuffer_pass.depth_stencil_attachment = gpu.RenderPassDepthStencilAttachment{ + .view = app.gbuffer.texture_views[2], + .depth_load_op = .clear, + .depth_store_op = .store, + .depth_clear_value = 1.0, + .stencil_clear_value = 1.0, + }; + + app.write_gbuffer_pass.descriptor = gpu.RenderPassDescriptor.init(.{ + .label = "write_gbuffer_pass", + .color_attachments = &app.write_gbuffer_pass.color_attachments, + .depth_stencil_attachment = &app.write_gbuffer_pass.depth_stencil_attachment, + }); + } + { + // Texture Quad Pass + app.texture_quad_pass.color_attachment = gpu.RenderPassColorAttachment{ + .clear_value = .{ + .r = 0.0, + .g = 0.0, + .b = 0.0, + .a = 1.0, + }, + .load_op = .clear, + .store_op = .store, + }; + + app.texture_quad_pass.descriptor = gpu.RenderPassDescriptor{ + .label = "texture_quad_pass(1)", + .color_attachment_count = 1, + .color_attachments = &[_]gpu.RenderPassColorAttachment{app.texture_quad_pass.color_attachment}, + }; + } +} + +fn prepareUniformBuffers(app: *App) void { + { + // Config uniform buffer + app.lights.config_uniform_buffer_size = @sizeOf(i32); + app.lights.config_uniform_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = app.lights.config_uniform_buffer_size, + .mapped_at_creation = .true, + }); + var config_data = app.lights.config_uniform_buffer.getMappedRange(i32, 0, 1).?; + config_data[0] = app.settings.lights_count; + app.lights.config_uniform_buffer.unmap(); + } + { + // Model uniform buffer + app.model_uniform_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = @sizeOf(Mat4) * 2, + }); + } + { + // Camera uniform buffer + app.camera_uniform_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = @sizeOf(Mat4) * 2, + }); + } + { + // Scene uniform bind group + const bind_group_entries = [_]gpu.BindGroup.Entry{ + .{ + .binding = 0, + .buffer = app.model_uniform_buffer, + .size = @sizeOf(Mat4) * 2, + }, + .{ + .binding = 1, + .buffer = app.camera_uniform_buffer, + .size = camera_uniform_buffer_size, + }, + }; + const bind_group_layout = app.write_gbuffers_pipeline.getBindGroupLayout(0); + app.scene_uniform_bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .label = "scene_uniform_bind_group", + .layout = bind_group_layout, + .entries = &bind_group_entries, + }), + ); + bind_group_layout.release(); + } + { + // Surface size uniform buffer + app.surface_size_uniform_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = @sizeOf(f32) * 4, + }); + } + { + // Surface size uniform bind group + const bind_group_entries = [_]gpu.BindGroup.Entry{ + .{ + .binding = 0, + .buffer = app.surface_size_uniform_buffer, + .size = @sizeOf(f32) * 2, + }, + }; + app.surface_size_uniform_bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = app.surface_size_uniform_bind_group_layout, + .entries = &bind_group_entries, + }), + ); + } + { + // GBuffer textures bind group + const bind_group_entries = [_]gpu.BindGroup.Entry{ + gpu.BindGroup.Entry.textureView(0, app.gbuffer.texture_views[0]), + gpu.BindGroup.Entry.textureView(1, app.gbuffer.texture_views[1]), + gpu.BindGroup.Entry.textureView(2, app.gbuffer.texture_views[2]), + }; + app.gbuffer_textures_bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = app.gbuffer_textures_bind_group_layout, + .entries = &bind_group_entries, + }), + ); + } +} + +fn prepareComputePipelineLayout(app: *App) void { + const bind_group_layouts = [_]*gpu.BindGroupLayout{app.lights.buffer_compute_bind_group_layout}; + app.light_update_compute_pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &bind_group_layouts, + })); +} + +fn prepareLightUpdateComputePipeline(app: *App) void { + const shader_module = core.device.createShaderModuleWGSL( + "lightUpdate.wgsl", + @embedFile("lightUpdate.wgsl"), + ); + app.light_update_compute_pipeline = core.device.createComputePipeline(&gpu.ComputePipeline.Descriptor{ + .compute = gpu.ProgrammableStageDescriptor{ + .module = shader_module, + .entry_point = "main", + }, + .layout = app.light_update_compute_pipeline_layout, + }); + shader_module.release(); +} + +fn prepareLights(app: *App) void { + // Lights data are uploaded in a storage buffer + // which could be updated/culled/etc. with a compute shader + const extent = comptime Vec3{ + light_extent_max[0] - light_extent_min[0], + light_extent_max[1] - light_extent_min[1], + light_extent_max[2] - light_extent_min[2], + }; + app.lights.buffer_size = @sizeOf(f32) * light_data_stride * max_num_lights; + app.lights.buffer = core.device.createBuffer(&.{ + .usage = .{ .storage = true }, + .size = app.lights.buffer_size, + .mapped_at_creation = .true, + }); + // We randomly populate lights randomly in a box range + // And simply move them along y-axis per frame to show they are dynamic lightings + var light_data = app.lights.buffer.getMappedRange(f32, 0, light_data_stride * max_num_lights).?; + + var xoroshiro = std.rand.Xoroshiro128.init(9273853284918); + const rng = std.rand.Random.init( + &xoroshiro, + std.rand.Xoroshiro128.fill, + ); + var i: usize = 0; + var offset: usize = 0; + while (i < max_num_lights) : (i += 1) { + offset = light_data_stride * i; + // Position + light_data[offset + 0] = rng.float(f32) * extent[0] + light_extent_min[0]; + light_data[offset + 1] = rng.float(f32) * extent[1] + light_extent_min[1]; + light_data[offset + 2] = rng.float(f32) * extent[2] + light_extent_min[2]; + light_data[offset + 3] = 1.0; + // Color + light_data[offset + 4] = rng.float(f32) * 2.0; + light_data[offset + 5] = rng.float(f32) * 2.0; + light_data[offset + 6] = rng.float(f32) * 2.0; + // Radius + light_data[offset + 7] = 20.0; + } + app.lights.buffer.unmap(); + // + // Lights extent buffer + // + app.lights.extent_buffer_size = @sizeOf(f32) * light_data_stride * max_num_lights; + app.lights.extent_buffer = core.device.createBuffer(&.{ + .usage = .{ .uniform = true, .copy_dst = true }, + .size = app.lights.extent_buffer_size, + }); + var light_extent_data = [1]f32{0.0} ** 8; + @memcpy(light_extent_data[0..3], &light_extent_min); + @memcpy(light_extent_data[4..7], &light_extent_max); + const queue = core.queue; + queue.writeBuffer( + app.lights.extent_buffer, + 0, + &light_extent_data, + ); + // + // Lights buffer bind group + // + { + const bind_group_entries = [_]gpu.BindGroup.Entry{ + .{ + .binding = 0, + .buffer = app.lights.buffer, + .size = app.lights.buffer_size, + }, + .{ + .binding = 1, + .buffer = app.lights.config_uniform_buffer, + .size = app.lights.config_uniform_buffer_size, + }, + .{ + .binding = 2, + .buffer = app.camera_uniform_buffer, + .size = camera_uniform_buffer_size, + }, + }; + app.lights.buffer_bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = app.lights.buffer_bind_group_layout, + .entries = &bind_group_entries, + }), + ); + } + // + // Lights buffer compute bind group + // + { + const bind_group_entries = [_]gpu.BindGroup.Entry{ + .{ + .binding = 0, + .buffer = app.lights.buffer, + .size = app.lights.buffer_size, + }, + .{ + .binding = 1, + .buffer = app.lights.config_uniform_buffer, + .size = app.lights.config_uniform_buffer_size, + }, + .{ + .binding = 2, + .buffer = app.lights.extent_buffer, + .size = app.lights.extent_buffer_size, + }, + }; + const bind_group_layout = app.light_update_compute_pipeline.getBindGroupLayout(0); + app.lights.buffer_compute_bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bind_group_layout, + .entries = &bind_group_entries, + }), + ); + bind_group_layout.release(); + } +} + +fn prepareViewMatrices(app: *App) void { + const screen_dimensions = Dimensions2D(f32){ + .width = @as(f32, @floatFromInt(app.screen_dimensions.width)), + .height = @as(f32, @floatFromInt(app.screen_dimensions.height)), + }; + const aspect: f32 = screen_dimensions.width / screen_dimensions.height; + const fov: f32 = 2.0 * std.math.pi / 5.0; + const znear: f32 = 1.0; + const zfar: f32 = 2000.0; + app.view_matrices.projection_matrix = zm.perspectiveFovRhGl(fov, aspect, znear, zfar); + const eye_position = zm.Vec{ 0.0, 50.0, -100.0, 0.0 }; + app.view_matrices.up_vector = zm.Vec{ 0.0, 1.0, 0.0, 0.0 }; + app.view_matrices.origin = zm.Vec{ 0.0, 0.0, 0.0, 0.0 }; + const view_matrix = zm.lookAtRh( + eye_position, + app.view_matrices.origin, + app.view_matrices.up_vector, + ); + const view_proj_matrix: zm.Mat = zm.mul(view_matrix, app.view_matrices.projection_matrix); + // Move the model so it's centered. + const model_matrix = zm.translationV(zm.Vec{ 0.0, -45.0, 0.0, 0.0 }); + const queue = core.queue; + queue.writeBuffer( + app.camera_uniform_buffer, + 0, + &view_proj_matrix, + ); + queue.writeBuffer( + app.model_uniform_buffer, + 0, + &model_matrix, + ); + const invert_transpose_model_matrix = zm.transpose(zm.inverse(model_matrix)); + queue.writeBuffer( + app.model_uniform_buffer, + @sizeOf(Mat4), + &invert_transpose_model_matrix, + ); + // Pass the surface size to shader to help sample from gBuffer textures using coord + const surface_size = Vec2{ screen_dimensions.width, screen_dimensions.height }; + queue.writeBuffer( + app.surface_size_uniform_buffer, + 0, + &surface_size, + ); +} + +fn buildCommandBuffer(app: *App) !*gpu.CommandBuffer { + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + defer back_buffer_view.release(); + + const encoder = core.device.createCommandEncoder(null); + defer encoder.release(); + + std.debug.assert(app.screen_dimensions.width == core.descriptor.width); + std.debug.assert(app.screen_dimensions.height == core.descriptor.height); + + const dimensions = Dimensions2D(f32){ + .width = @as(f32, @floatFromInt(core.descriptor.width)), + .height = @as(f32, @floatFromInt(core.descriptor.height)), + }; + + { + // Write position, normal, albedo etc. data to gBuffers + const pass = encoder.beginRenderPass(&app.write_gbuffer_pass.descriptor); + pass.setViewport( + 0, + 0, + dimensions.width, + dimensions.height, + 0.0, + 1.0, + ); + pass.setScissorRect(0, 0, core.descriptor.width, core.descriptor.height); + pass.setPipeline(app.write_gbuffers_pipeline); + pass.setBindGroup(0, app.scene_uniform_bind_group, null); + pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * app.vertex_count); + pass.setIndexBuffer(app.index_buffer, .uint16, 0, @sizeOf(u16) * app.index_count); + pass.drawIndexed( + app.index_count, + 1, // instance_count + 0, // first_index + 0, // base_vertex + 0, // first_instance + ); + pass.end(); + pass.release(); + } + { + // Update lights position + const pass = encoder.beginComputePass(null); + pass.setPipeline(app.light_update_compute_pipeline); + pass.setBindGroup(0, app.lights.buffer_compute_bind_group, null); + pass.dispatchWorkgroups(@divExact(max_num_lights, 64), 1, 1); + pass.end(); + pass.release(); + } + app.texture_quad_pass.color_attachment.view = back_buffer_view; + app.texture_quad_pass.descriptor = gpu.RenderPassDescriptor{ + .label = "texture_quad_pass(0)", + .color_attachment_count = 1, + .color_attachments = &[_]gpu.RenderPassColorAttachment{app.texture_quad_pass.color_attachment}, + }; + + const pass = encoder.beginRenderPass(&app.texture_quad_pass.descriptor); + switch (app.settings.render_mode) { + .gbuffer_view => { + // GBuffers debug view + // Left: position + // Middle: normal + // Right: albedo (use uv to mimic a checkerboard texture) + pass.setPipeline(app.gbuffers_debug_view_pipeline); + pass.setBindGroup(0, app.gbuffer_textures_bind_group, null); + pass.setBindGroup(1, app.surface_size_uniform_bind_group, null); + pass.draw(6, 1, 0, 0); + }, + else => { + // Deferred rendering + pass.setPipeline(app.deferred_render_pipeline); + pass.setBindGroup(0, app.gbuffer_textures_bind_group, null); + pass.setBindGroup(1, app.lights.buffer_bind_group, null); + pass.setBindGroup(2, app.surface_size_uniform_bind_group, null); + pass.draw(6, 1, 0, 0); + }, + } + + pass.end(); + pass.release(); + + return encoder.finish(null); +} + +const modes = [_][:0]const u8{ "rendering", "gbuffers view" }; + +fn printControls(app: *App) void { + std.debug.print("[controls]\n", .{}); + std.debug.print("[p] paused: {}\n", .{app.is_paused}); + std.debug.print("[m] mode: {s}\n", .{modes[@intFromEnum(app.settings.render_mode)]}); + std.debug.print("[,] decrease lights: {}\n", .{app.settings.lights_count}); + std.debug.print("[.] increase lights: {}\n", .{app.settings.lights_count}); +} + +fn updateUI(app: *App, event: core.Event) void { + switch (event) { + .key_press => |ev| { + var update_lights = false; + switch (ev.key) { + .p => app.is_paused = !app.is_paused, + .m => { + const mode_index = @intFromEnum(app.settings.render_mode); + app.settings.render_mode = @enumFromInt((mode_index + 1) % modes.len); + }, + .comma => { + update_lights = true; + if (app.settings.lights_count >= 25) app.settings.lights_count -= 25; + }, + .period => { + update_lights = true; + app.settings.lights_count += 25; + }, + else => return, + } + + if (update_lights) core.queue.writeBuffer( + app.lights.config_uniform_buffer, + 0, + &[1]i32{app.settings.lights_count}, + ); + app.printControls(); + }, + else => {}, + } +} + +// TODO +// fn drawUI(app: *App) void { +// if (imgui.beginCombo("Mode", .{ .preview_value = modes[mode_index] })) { +// for (modes, 0..) |mode, mode_i| { +// const i = @as(u32, @intCast(mode_i)); +// if (imgui.selectable(mode, .{ .selected = mode_index == i })) { +// app.settings.render_mode = @as(RenderMode, @enumFromInt(mode_i)); +// } +// } +// } +// if (imgui.sliderInt("Light count", .{ .v = &app.settings.lights_count, .min = 1, .max = max_num_lights })) { +// queue.writeBuffer( +// app.lights.config_uniform_buffer, +// 0, +// &[1]i32{app.settings.lights_count}, +// ); +// } +// imgui.end(); +// } + +fn updateUniformBuffers(app: *App) void { + core.device.tick(); + app.camera_rotation += toRadians(360.0) * (app.delta_time / 5.0); // one rotation every 5s + const rotation = zm.rotationY(app.camera_rotation); + const eye_position = zm.mul(rotation, zm.Vec{ 0, 50, -100, 0 }); + const view_matrix = zm.lookAtRh(eye_position, app.view_matrices.origin, app.view_matrices.up_vector); + app.view_matrices.view_proj_matrix = zm.mul(view_matrix, app.view_matrices.projection_matrix); + const queue = core.queue; + queue.writeBuffer( + app.camera_uniform_buffer, + 0, + &app.view_matrices.view_proj_matrix, + ); + + const inv_view_proj_matrix = zm.inverse(app.view_matrices.view_proj_matrix); + queue.writeBuffer( + app.camera_uniform_buffer, + @sizeOf(Mat4), + &inv_view_proj_matrix, + ); +} + +inline fn roundToMultipleOf4(comptime T: type, value: T) T { + return (value + 3) & ~@as(T, 3); +} + +inline fn toRadians(degrees: f32) f32 { + return degrees * (std.math.pi / 180.0); +} diff --git a/src/core/examples/deferred-rendering/vertexTextureQuad.wgsl b/src/core/examples/deferred-rendering/vertexTextureQuad.wgsl new file mode 100644 index 00000000..c2cfb18b --- /dev/null +++ b/src/core/examples/deferred-rendering/vertexTextureQuad.wgsl @@ -0,0 +1,11 @@ +@vertex +fn main( + @builtin(vertex_index) VertexIndex : u32 +) -> @builtin(position) vec4 { + const pos = array( + vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0), + vec2(-1.0, 1.0), vec2(1.0, -1.0), vec2(1.0, 1.0), + ); + + return vec4(pos[VertexIndex], 0.0, 1.0); +} diff --git a/src/core/examples/deferred-rendering/vertexWriteGBuffers.wgsl b/src/core/examples/deferred-rendering/vertexWriteGBuffers.wgsl new file mode 100644 index 00000000..eb3b930e --- /dev/null +++ b/src/core/examples/deferred-rendering/vertexWriteGBuffers.wgsl @@ -0,0 +1,30 @@ +struct Uniforms { + modelMatrix : mat4x4, + normalModelMatrix : mat4x4, +} +struct Camera { + viewProjectionMatrix : mat4x4, + invViewProjectionMatrix : mat4x4, +} +@group(0) @binding(0) var uniforms : Uniforms; +@group(0) @binding(1) var camera : Camera; + +struct VertexOutput { + @builtin(position) Position : vec4, + @location(0) fragNormal: vec3, // normal in world space + @location(1) fragUV: vec2, +} + +@vertex +fn main( + @location(0) position : vec3, + @location(1) normal : vec3, + @location(2) uv : vec2 +) -> VertexOutput { + var output : VertexOutput; + let worldPosition = (uniforms.modelMatrix * vec4(position, 1.0)).xyz; + output.Position = camera.viewProjectionMatrix * vec4(worldPosition, 1.0); + output.fragNormal = normalize((uniforms.normalModelMatrix * vec4(normal, 1.0)).xyz); + output.fragUV = uv; + return output; +} diff --git a/src/core/examples/deferred-rendering/vertex_writer.zig b/src/core/examples/deferred-rendering/vertex_writer.zig new file mode 100644 index 00000000..1610981e --- /dev/null +++ b/src/core/examples/deferred-rendering/vertex_writer.zig @@ -0,0 +1,188 @@ +const std = @import("std"); + +/// Vertex writer manages the placement of vertices by tracking which are unique. If a duplicate vertex is added +/// with `put`, only it's index will be written to the index buffer. +/// `IndexType` should match the integer type used for the index buffer +pub fn VertexWriter(comptime VertexType: type, comptime IndexType: type) type { + return struct { + const MapEntry = struct { + packed_index: IndexType = null_index, + next_sparse: IndexType = null_index, + }; + + const null_index: IndexType = std.math.maxInt(IndexType); + + vertices: []VertexType, + indices: []IndexType, + sparse_to_packed_map: []MapEntry, + + /// Next index outside of the 1:1 mapping range for storing + /// position -> normal collisions + next_collision_index: IndexType, + + /// Next packed index + next_packed_index: IndexType, + written_indices_count: IndexType, + + /// Allocate storage and set default values + /// `sparse_vertices_count` is the number of vertices in the source before de-duplication / remapping + /// Put more succinctly, the largest index value in source index buffer + /// `max_vertex_count` is largest permutation of vertices assuming that {vertex, uv, normal} never map 1:1 and always + /// create a new mapping + pub fn init( + allocator: std.mem.Allocator, + indices_count: IndexType, + sparse_vertices_count: IndexType, + max_vertex_count: IndexType, + ) !@This() { + var result: @This() = undefined; + result.vertices = try allocator.alloc(VertexType, max_vertex_count); + result.indices = try allocator.alloc(IndexType, indices_count); + result.sparse_to_packed_map = try allocator.alloc(MapEntry, max_vertex_count); + result.next_collision_index = sparse_vertices_count; + result.next_packed_index = 0; + result.written_indices_count = 0; + @memset(result.sparse_to_packed_map, .{}); + return result; + } + + pub fn put(self: *@This(), vertex: VertexType, sparse_index: IndexType) void { + if (self.sparse_to_packed_map[sparse_index].packed_index == null_index) { + // New start of chain, reserve a new packed index and add entry to `index_map` + const packed_index = self.next_packed_index; + self.sparse_to_packed_map[sparse_index].packed_index = packed_index; + self.vertices[packed_index] = vertex; + self.indices[self.written_indices_count] = packed_index; + self.written_indices_count += 1; + self.next_packed_index += 1; + return; + } + var previous_sparse_index: IndexType = undefined; + var current_sparse_index = sparse_index; + while (current_sparse_index != null_index) { + const packed_index = self.sparse_to_packed_map[current_sparse_index].packed_index; + if (std.mem.eql(u8, &std.mem.toBytes(self.vertices[packed_index]), &std.mem.toBytes(vertex))) { + // We already have a record for this vertex in our chain + self.indices[self.written_indices_count] = packed_index; + self.written_indices_count += 1; + return; + } + previous_sparse_index = current_sparse_index; + current_sparse_index = self.sparse_to_packed_map[current_sparse_index].next_sparse; + } + // This is a new mapping for the given sparse index + const packed_index = self.next_packed_index; + const remapped_sparse_index = self.next_collision_index; + self.indices[self.written_indices_count] = packed_index; + self.vertices[packed_index] = vertex; + self.sparse_to_packed_map[previous_sparse_index].next_sparse = remapped_sparse_index; + self.sparse_to_packed_map[remapped_sparse_index].packed_index = packed_index; + self.next_packed_index += 1; + self.next_collision_index += 1; + self.written_indices_count += 1; + } + + pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void { + allocator.free(self.vertices); + allocator.free(self.indices); + allocator.free(self.sparse_to_packed_map); + } + + pub fn indexBuffer(self: @This()) []IndexType { + return self.indices; + } + + pub fn vertexBuffer(self: @This()) []VertexType { + return self.vertices[0..self.next_packed_index]; + } + }; +} + +test "VertexWriter" { + const Vec3 = [3]f32; + const Vertex = extern struct { + position: Vec3, + normal: Vec3, + }; + + const expect = std.testing.expect; + const allocator = std.testing.allocator; + + const Face = struct { + position: [3]u16, + normal: [3]u16, + }; + + const vertices = [_]Vec3{ + Vec3{ 1.0, 0.0, 0.0 }, // 0: Position + Vec3{ 2.0, 0.0, 0.0 }, // 1: Position + Vec3{ 3.0, 0.0, 0.0 }, // 2: Position + Vec3{ 1.0, 0.0, 0.0 }, // 3: Normal + Vec3{ 4.0, 0.0, 0.0 }, // 4: Position + Vec3{ 0.0, 1.0, 0.0 }, // 5: Normal + Vec3{ 5.0, 0.0, 0.0 }, // 6: Position + Vec3{ 0.0, 0.0, 1.0 }, // 7: Normal + Vec3{ 1.0, 0.0, 1.0 }, // 8: Normal + Vec3{ 6.0, 0.0, 0.0 }, // 9: Position + }; + + const faces = [_]Face{ + .{ .position = .{ 0, 4, 2 }, .normal = .{ 7, 5, 3 } }, + .{ .position = .{ 2, 3, 9 }, .normal = .{ 3, 7, 8 } }, + .{ .position = .{ 9, 2, 4 }, .normal = .{ 8, 7, 5 } }, + .{ .position = .{ 2, 6, 1 }, .normal = .{ 3, 5, 7 } }, + .{ .position = .{ 9, 6, 0 }, .normal = .{ 5, 7, 8 } }, + }; + + var writer = try VertexWriter(Vertex, u32).init( + allocator, + faces.len * 3, // indices count + vertices.len, // original vertices count + faces.len * 3, // maximum vertices count + ); + defer writer.deinit(allocator); + + for (faces) |face| { + var x: usize = 0; + while (x < 3) : (x += 1) { + const position_index = face.position[x]; + const position = vertices[position_index]; + const normal = vertices[face.normal[x]]; + const vertex = Vertex{ + .position = position, + .normal = normal, + }; + writer.put(vertex, position_index); + } + } + + const indices = writer.indexBuffer(); + try expect(indices.len == faces.len * 3); + + // Face 0 + try expect(indices[0] == 0); // (0, 7) New + try expect(indices[1] == 1); // (4, 5) New + try expect(indices[2] == 2); // (2, 3) New + + // Face 1 + try expect(indices[3 + 0] == 2); // (2, 3) Duplicate - Reuse index + try expect(indices[3 + 1] == 3); // (3, 7) New + try expect(indices[3 + 2] == 4); // (9, 8) New + + // Face 2 + try expect(indices[6 + 0] == 4); // (9, 8) Duplicate - Reuse index + try expect(indices[6 + 1] == 5); // (2, 7) New normal mapping (Don't clobber) + try expect(indices[6 + 2] == 1); // (4, 5) Duplicate - Reuse Index + + // Face 3 + try expect(indices[9 + 0] == 2); // (2, 3) Duplicate - Reuse index + try expect(indices[9 + 1] == 6); // (6, 5) New + try expect(indices[9 + 2] == 7); // (1, 7) New + + // Face 4 + try expect(indices[12 + 0] == 8); // (9, 5) New normal mapping (Don't clobber) + try expect(indices[12 + 1] == 9); // (6, 7) New normal mapping (Don't clobber) + try expect(indices[12 + 2] == 10); // (0, 8) New normal mapping (Don't clobber) + + try expect(writer.vertexBuffer().len == 11); +} diff --git a/src/core/examples/fractal-cube/cube_mesh.zig b/src/core/examples/fractal-cube/cube_mesh.zig new file mode 100644 index 00000000..f26c75ac --- /dev/null +++ b/src/core/examples/fractal-cube/cube_mesh.zig @@ -0,0 +1,49 @@ +pub const Vertex = extern struct { + pos: @Vector(4, f32), + col: @Vector(4, f32), + uv: @Vector(2, f32), +}; + +pub const vertices = [_]Vertex{ + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, +}; diff --git a/src/core/examples/fractal-cube/main.zig b/src/core/examples/fractal-cube/main.zig new file mode 100755 index 00000000..e793e6e4 --- /dev/null +++ b/src/core/examples/fractal-cube/main.zig @@ -0,0 +1,371 @@ +//! To get the effect we want, we need a texture on which to render; +//! we can't use the swapchain texture directly, but we can get the effect +//! by doing the same render pass twice, on the texture and the swapchain. +//! We also need a second texture to use on the cube (after the render pass +//! it needs to copy the other texture.) We can't use the same texture since +//! it would interfere with the synchronization on the gpu during the render pass. +//! This demo currently does not work on opengl, because core.descriptor.width/height, +//! are set to 0 after core.init() and because webgpu does not implement copyTextureToTexture, +//! for opengl + +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const zm = @import("zmath"); +const Vertex = @import("cube_mesh.zig").Vertex; +const vertices = @import("cube_mesh.zig").vertices; + +pub const App = @This(); + +const UniformBufferObject = struct { + mat: zm.Mat, +}; + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +title_timer: core.Timer, +timer: core.Timer, +pipeline: *gpu.RenderPipeline, +vertex_buffer: *gpu.Buffer, +uniform_buffer: *gpu.Buffer, +bind_group: *gpu.BindGroup, +depth_texture: ?*gpu.Texture, +depth_texture_view: *gpu.TextureView, +cube_texture: *gpu.Texture, +cube_texture_view: *gpu.TextureView, +cube_texture_render: *gpu.Texture, +cube_texture_view_render: *gpu.TextureView, +sampler: *gpu.Sampler, +bgl: *gpu.BindGroupLayout, + +pub fn init(app: *App) !void { + try core.init(.{}); + + const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); + + const vertex_attributes = [_]gpu.VertexAttribute{ + .{ .format = .float32x4, .offset = @offsetOf(Vertex, "pos"), .shader_location = 0 }, + .{ .format = .float32x2, .offset = @offsetOf(Vertex, "uv"), .shader_location = 1 }, + }; + const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{ + .array_stride = @sizeOf(Vertex), + .attributes = &vertex_attributes, + }); + + const blend = gpu.BlendState{}; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + + const bgle_buffer = gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, true, 0); + const bgle_sampler = gpu.BindGroupLayout.Entry.sampler(1, .{ .fragment = true }, .filtering); + const bgle_textureview = gpu.BindGroupLayout.Entry.texture(2, .{ .fragment = true }, .float, .dimension_2d, false); + const bgl = core.device.createBindGroupLayout( + &gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &.{ bgle_buffer, bgle_sampler, bgle_textureview }, + }), + ); + + const bind_group_layouts = [_]*gpu.BindGroupLayout{bgl}; + const pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &bind_group_layouts, + })); + + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + .layout = pipeline_layout, + .depth_stencil = &.{ + .format = .depth24_plus, + .depth_write_enabled = .true, + .depth_compare = .less, + }, + .vertex = gpu.VertexState.init(.{ + .module = shader_module, + .entry_point = "vertex_main", + .buffers = &.{vertex_buffer_layout}, + }), + .primitive = .{ + .cull_mode = .back, + }, + }; + + const vertex_buffer = core.device.createBuffer(&.{ + .usage = .{ .vertex = true }, + .size = @sizeOf(Vertex) * vertices.len, + .mapped_at_creation = .true, + }); + const vertex_mapped = vertex_buffer.getMappedRange(Vertex, 0, vertices.len); + @memcpy(vertex_mapped.?, vertices[0..]); + vertex_buffer.unmap(); + + const uniform_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = @sizeOf(UniformBufferObject), + .mapped_at_creation = .false, + }); + + // The texture to put on the cube + const cube_texture = core.device.createTexture(&gpu.Texture.Descriptor{ + .usage = .{ .texture_binding = true, .copy_dst = true }, + .size = .{ .width = core.descriptor.width, .height = core.descriptor.height }, + .format = core.descriptor.format, + }); + // The texture on which we render + const cube_texture_render = core.device.createTexture(&gpu.Texture.Descriptor{ + .usage = .{ .render_attachment = true, .copy_src = true }, + .size = .{ .width = core.descriptor.width, .height = core.descriptor.height }, + .format = core.descriptor.format, + }); + + const sampler = core.device.createSampler(&gpu.Sampler.Descriptor{ + .mag_filter = .linear, + .min_filter = .linear, + }); + + const cube_texture_view = cube_texture.createView(&gpu.TextureView.Descriptor{ + .format = core.descriptor.format, + .dimension = .dimension_2d, + .mip_level_count = 1, + .array_layer_count = 1, + }); + const cube_texture_view_render = cube_texture_render.createView(&gpu.TextureView.Descriptor{ + .format = core.descriptor.format, + .dimension = .dimension_2d, + .mip_level_count = 1, + .array_layer_count = 1, + }); + + const bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bgl, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, uniform_buffer, 0, @sizeOf(UniformBufferObject)), + gpu.BindGroup.Entry.sampler(1, sampler), + gpu.BindGroup.Entry.textureView(2, cube_texture_view), + }, + }), + ); + + const depth_texture = core.device.createTexture(&gpu.Texture.Descriptor{ + .usage = .{ .render_attachment = true }, + .size = .{ .width = core.descriptor.width, .height = core.descriptor.height }, + .format = .depth24_plus, + }); + const depth_texture_view = depth_texture.createView(&gpu.TextureView.Descriptor{ + .format = .depth24_plus, + .dimension = .dimension_2d, + .array_layer_count = 1, + .mip_level_count = 1, + }); + + app.timer = try core.Timer.start(); + app.title_timer = try core.Timer.start(); + app.pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + app.vertex_buffer = vertex_buffer; + app.uniform_buffer = uniform_buffer; + app.bind_group = bind_group; + app.depth_texture = depth_texture; + app.depth_texture_view = depth_texture_view; + app.cube_texture = cube_texture; + app.cube_texture_view = cube_texture_view; + app.cube_texture_render = cube_texture_render; + app.cube_texture_view_render = cube_texture_view_render; + app.sampler = sampler; + app.bgl = bgl; + + shader_module.release(); + pipeline_layout.release(); +} + +pub fn deinit(app: *App) void { + defer _ = gpa.deinit(); + defer core.deinit(); + + app.pipeline.release(); + app.bgl.release(); + app.vertex_buffer.release(); + app.uniform_buffer.release(); + app.cube_texture.release(); + app.cube_texture_render.release(); + app.sampler.release(); + app.cube_texture_view.release(); + app.cube_texture_view_render.release(); + app.bind_group.release(); + app.depth_texture.?.release(); + app.depth_texture_view.release(); +} + +pub fn update(app: *App) !bool { + var iter = core.pollEvents(); + while (iter.next()) |event| { + switch (event) { + .key_press => |ev| { + if (ev.key == .space) return true; + }, + .close => return true, + .framebuffer_resize => |ev| { + app.depth_texture.?.release(); + app.depth_texture = core.device.createTexture(&gpu.Texture.Descriptor{ + .usage = .{ .render_attachment = true }, + .size = .{ .width = ev.width, .height = ev.height }, + .format = .depth24_plus, + }); + + app.cube_texture.release(); + app.cube_texture = core.device.createTexture(&gpu.Texture.Descriptor{ + .usage = .{ .texture_binding = true, .copy_dst = true }, + .size = .{ .width = ev.width, .height = ev.height }, + .format = core.descriptor.format, + }); + app.cube_texture_render.release(); + app.cube_texture_render = core.device.createTexture(&gpu.Texture.Descriptor{ + .usage = .{ .render_attachment = true, .copy_src = true }, + .size = .{ .width = ev.width, .height = ev.height }, + .format = core.descriptor.format, + }); + + app.depth_texture_view.release(); + app.depth_texture_view = app.depth_texture.?.createView(&gpu.TextureView.Descriptor{ + .format = .depth24_plus, + .dimension = .dimension_2d, + .array_layer_count = 1, + .mip_level_count = 1, + }); + + app.cube_texture_view.release(); + app.cube_texture_view = app.cube_texture.createView(&gpu.TextureView.Descriptor{ + .format = core.descriptor.format, + .dimension = .dimension_2d, + .mip_level_count = 1, + .array_layer_count = 1, + }); + app.cube_texture_view_render.release(); + app.cube_texture_view_render = app.cube_texture_render.createView(&gpu.TextureView.Descriptor{ + .format = core.descriptor.format, + .dimension = .dimension_2d, + .mip_level_count = 1, + .array_layer_count = 1, + }); + + app.bind_group.release(); + app.bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = app.bgl, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, app.uniform_buffer, 0, @sizeOf(UniformBufferObject)), + gpu.BindGroup.Entry.sampler(1, app.sampler), + gpu.BindGroup.Entry.textureView(2, app.cube_texture_view), + }, + }), + ); + }, + else => {}, + } + } + + const cube_view = app.cube_texture_view_render; + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + + const cube_color_attachment = gpu.RenderPassColorAttachment{ + .view = cube_view, + .clear_value = gpu.Color{ .r = 0.5, .g = 0.5, .b = 0.5, .a = 1 }, + .load_op = .clear, + .store_op = .store, + }; + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = gpu.Color{ .r = 0.5, .g = 0.5, .b = 0.5, .a = 1 }, + .load_op = .clear, + .store_op = .store, + }; + + const depth_stencil_attachment = gpu.RenderPassDepthStencilAttachment{ + .view = app.depth_texture_view, + .depth_load_op = .clear, + .depth_store_op = .store, + .depth_clear_value = 1.0, + }; + + const encoder = core.device.createCommandEncoder(null); + const cube_render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{cube_color_attachment}, + .depth_stencil_attachment = &depth_stencil_attachment, + }); + const render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + .depth_stencil_attachment = &depth_stencil_attachment, + }); + + { + const time = app.timer.read(); + const model = zm.mul(zm.rotationX(time * (std.math.pi / 2.0)), zm.rotationZ(time * (std.math.pi / 2.0))); + const view = zm.lookAtRh( + zm.Vec{ 0, -4, 0, 1 }, + zm.Vec{ 0, 0, 0, 1 }, + zm.Vec{ 0, 0, 1, 0 }, + ); + const proj = zm.perspectiveFovRh( + (std.math.pi * 2.0 / 5.0), + @as(f32, @floatFromInt(core.descriptor.width)) / @as(f32, @floatFromInt(core.descriptor.height)), + 1, + 100, + ); + const ubo = UniformBufferObject{ + .mat = zm.transpose(zm.mul(zm.mul(model, view), proj)), + }; + encoder.writeBuffer(app.uniform_buffer, 0, &[_]UniformBufferObject{ubo}); + } + + const pass = encoder.beginRenderPass(&render_pass_info); + pass.setPipeline(app.pipeline); + pass.setBindGroup(0, app.bind_group, &.{0}); + pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len); + pass.draw(vertices.len, 1, 0, 0); + pass.end(); + pass.release(); + + encoder.copyTextureToTexture( + &gpu.ImageCopyTexture{ + .texture = app.cube_texture_render, + }, + &gpu.ImageCopyTexture{ + .texture = app.cube_texture, + }, + &.{ .width = core.descriptor.width, .height = core.descriptor.height }, + ); + + const cube_pass = encoder.beginRenderPass(&cube_render_pass_info); + cube_pass.setPipeline(app.pipeline); + cube_pass.setBindGroup(0, app.bind_group, &.{0}); + cube_pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len); + cube_pass.draw(vertices.len, 1, 0, 0); + cube_pass.end(); + cube_pass.release(); + + var command = encoder.finish(null); + encoder.release(); + + const queue = core.queue; + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Fractal Cube [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} diff --git a/src/core/examples/fractal-cube/shader.wgsl b/src/core/examples/fractal-cube/shader.wgsl new file mode 100644 index 00000000..d38f0b4e --- /dev/null +++ b/src/core/examples/fractal-cube/shader.wgsl @@ -0,0 +1,36 @@ +struct Uniforms { + matrix : mat4x4, +}; + +@binding(0) @group(0) var ubo : Uniforms; + +struct VertexOut { + @builtin(position) Position : vec4, + @location(0) fragUV : vec2, + @location(1) fragPosition: vec4, +} + +@vertex fn vertex_main( + @location(0) position : vec4, + @location(1) uv: vec2 +) -> VertexOut { + var output : VertexOut; + output.Position = position * ubo.matrix; + output.fragUV = uv; + output.fragPosition = 0.5 * (position + vec4(1.0, 1.0, 1.0, 1.0)); + return output; +} + +@binding(1) @group(0) var mySampler: sampler; +@binding(2) @group(0) var myTexture: texture_2d; + +@fragment fn frag_main( + @location(0) fragUV: vec2, + @location(1) fragPosition: vec4 +) -> @location(0) vec4 { + let texColor = textureSample(myTexture, mySampler, fragUV * 0.8 + vec2(0.1, 0.1)); + let f = f32(length(texColor.rgb - vec3(0.5, 0.5, 0.5)) < 0.01); + return (1.0 - f) * texColor + f * fragPosition; + // return vec4(texColor.rgb,1.0); +} + diff --git a/src/core/examples/gen-texture-light/cube.wgsl b/src/core/examples/gen-texture-light/cube.wgsl new file mode 100644 index 00000000..4081f2dc --- /dev/null +++ b/src/core/examples/gen-texture-light/cube.wgsl @@ -0,0 +1,75 @@ +struct CameraUniform { + pos: vec4, + view_proj: mat4x4, +}; + +struct InstanceInput { + @location(3) model_matrix_0: vec4, + @location(4) model_matrix_1: vec4, + @location(5) model_matrix_2: vec4, + @location(6) model_matrix_3: vec4, +}; + +struct VertexInput { + @location(0) position: vec3, + @location(1) normal: vec3, + @location(2) tex_coords: vec2, +}; + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) tex_coords: vec2, + @location(1) normal: vec3, + @location(2) position: vec3, +}; + +struct Light { + position: vec4, + color: vec4, +}; + +@group(0) @binding(0) var camera: CameraUniform; +@group(1) @binding(0) var t_diffuse: texture_2d; +@group(1) @binding(1) var s_diffuse: sampler; +@group(2) @binding(0) var light: Light; + +@vertex +fn vs_main(model: VertexInput, instance: InstanceInput) -> VertexOutput { + let model_matrix = mat4x4( + instance.model_matrix_0, + instance.model_matrix_1, + instance.model_matrix_2, + instance.model_matrix_3, + ); + var out: VertexOutput; + let world_pos = model_matrix * vec4(model.position, 1.0); + out.position = world_pos.xyz; + out.normal = (model_matrix * vec4(model.normal, 0.0)).xyz; + out.clip_position = camera.view_proj * world_pos; + out.tex_coords = model.tex_coords; + return out; +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + let object_color = textureSample(t_diffuse, s_diffuse, in.tex_coords); + + let ambient = 0.1; + let ambient_color = light.color.rbg * ambient; + + let light_dir = normalize(light.position.xyz - in.position); + let diffuse = max(dot(in.normal, light_dir), 0.0); + let diffuse_color = light.color.rgb * diffuse; + + let view_dir = normalize(camera.pos.xyz - in.position); + let half_dir = normalize(view_dir + light_dir); + let specular = pow(max(dot(in.normal, half_dir), 0.0), 32.0); + let specular_color = light.color.rbg * specular; + + let all = ambient_color + diffuse_color + specular_color; + + let result = all * object_color.rgb; + + return vec4(result, object_color.a); + +} diff --git a/src/core/examples/gen-texture-light/light.wgsl b/src/core/examples/gen-texture-light/light.wgsl new file mode 100644 index 00000000..e110af20 --- /dev/null +++ b/src/core/examples/gen-texture-light/light.wgsl @@ -0,0 +1,35 @@ +struct CameraUniform { + view_pos: vec4, + view_proj: mat4x4, +}; + +struct VertexInput { + @location(0) position: vec3, + @location(1) normal: vec3, + @location(2) tex_coords: vec2, +}; + +struct VertexOutput { + @builtin(position) clip_position: vec4, +}; + +struct Light { + position: vec4, + color: vec4, +}; + +@group(0) @binding(0) var camera: CameraUniform; +@group(1) @binding(0) var light: Light; + +@vertex +fn vs_main(model: VertexInput) -> VertexOutput { + var out: VertexOutput; + let world_pos = vec4(model.position + light.position.xyz, 1.0); + out.clip_position = camera.view_proj * world_pos; + return out; +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + return vec4(1.0, 1.0, 1.0, 0.5); +} diff --git a/src/core/examples/gen-texture-light/main.zig b/src/core/examples/gen-texture-light/main.zig new file mode 100755 index 00000000..db4ebf7d --- /dev/null +++ b/src/core/examples/gen-texture-light/main.zig @@ -0,0 +1,891 @@ +// in this example: +// - comptime generated image data for texture +// - Blinn-Phong lighting +// - several pipelines +// +// quit with escape, q or space +// move camera with arrows or wasd + +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const zm = @import("zmath"); + +const Vec = zm.Vec; +const Mat = zm.Mat; +const Quat = zm.Quat; + +pub const App = @This(); + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +title_timer: core.Timer, +timer: core.Timer, +cube: Cube, +camera: Camera, +light: Light, +depth: Texture, +keys: u8, + +const Dir = struct { + const up: u8 = 0b0001; + const down: u8 = 0b0010; + const left: u8 = 0b0100; + const right: u8 = 0b1000; +}; + +pub fn init(app: *App) !void { + try core.init(.{}); + + app.title_timer = try core.Timer.start(); + app.timer = try core.Timer.start(); + + const eye = Vec{ 5.0, 7.0, 5.0, 0.0 }; + const target = Vec{ 0.0, 0.0, 0.0, 0.0 }; + + const framebuffer = core.descriptor; + const aspect_ratio = @as(f32, @floatFromInt(framebuffer.width)) / @as(f32, @floatFromInt(framebuffer.height)); + + app.cube = Cube.init(); + app.light = Light.init(); + app.depth = Texture.depth(core.device, framebuffer.width, framebuffer.height); + app.camera = Camera.init(core.device, eye, target, zm.Vec{ 0.0, 1.0, 0.0, 0.0 }, aspect_ratio, 45.0, 0.1, 100.0); + app.keys = 0; +} + +pub fn deinit(app: *App) void { + defer _ = gpa.deinit(); + defer core.deinit(); + + app.cube.deinit(); + app.camera.deinit(); + app.light.deinit(); + app.depth.release(); +} + +pub fn update(app: *App) !bool { + const delta_time = app.timer.lap(); + + var iter = core.pollEvents(); + while (iter.next()) |event| { + switch (event) { + .key_press => |ev| switch (ev.key) { + .q, .escape, .space => return true, + .w, .up => { + app.keys |= Dir.up; + }, + .s, .down => { + app.keys |= Dir.down; + }, + .a, .left => { + app.keys |= Dir.left; + }, + .d, .right => { + app.keys |= Dir.right; + }, + .one => core.setDisplayMode(.windowed), + .two => core.setDisplayMode(.fullscreen), + .three => core.setDisplayMode(.borderless), + else => {}, + }, + .key_release => |ev| switch (ev.key) { + .w, .up => { + app.keys &= ~Dir.up; + }, + .s, .down => { + app.keys &= ~Dir.down; + }, + .a, .left => { + app.keys &= ~Dir.left; + }, + .d, .right => { + app.keys &= ~Dir.right; + }, + else => {}, + }, + .framebuffer_resize => |ev| { + // recreates the sampler, which is a waste, but for an example it's ok + app.depth.release(); + app.depth = Texture.depth(core.device, ev.width, ev.height); + }, + .close => return true, + else => {}, + } + } + + // move camera + const speed = zm.Vec{ delta_time * 5, delta_time * 5, delta_time * 5, delta_time * 5 }; + const fwd = zm.normalize3(app.camera.target - app.camera.eye); + const right = zm.normalize3(zm.cross3(fwd, app.camera.up)); + + if (app.keys & Dir.up != 0) + app.camera.eye += fwd * speed; + + if (app.keys & Dir.down != 0) + app.camera.eye -= fwd * speed; + + if (app.keys & Dir.right != 0) + app.camera.eye += right * speed + else if (app.keys & Dir.left != 0) + app.camera.eye -= right * speed + else + app.camera.eye += right * (speed * @Vector(4, f32){ 0.5, 0.5, 0.5, 0.5 }); + + const queue = core.queue; + app.camera.update(queue); + + // move light + const light_speed = delta_time * 2.5; + app.light.update(queue, light_speed); + + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + defer back_buffer_view.release(); + + const encoder = core.device.createCommandEncoder(null); + defer encoder.release(); + + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = gpu.Color{ .r = 0.0, .g = 0.0, .b = 0.4, .a = 1.0 }, + .load_op = .clear, + .store_op = .store, + }; + + const render_pass_descriptor = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + .depth_stencil_attachment = &.{ + .view = app.depth.view, + .depth_load_op = .clear, + .depth_store_op = .store, + .depth_clear_value = 1.0, + }, + }); + + const pass = encoder.beginRenderPass(&render_pass_descriptor); + defer pass.release(); + + // brick cubes + pass.setPipeline(app.cube.pipeline); + pass.setBindGroup(0, app.camera.bind_group, &.{}); + pass.setBindGroup(1, app.cube.texture.bind_group.?, &.{}); + pass.setBindGroup(2, app.light.bind_group, &.{}); + pass.setVertexBuffer(0, app.cube.mesh.buffer, 0, app.cube.mesh.size); + pass.setVertexBuffer(1, app.cube.instance.buffer, 0, app.cube.instance.size); + pass.draw(4, app.cube.instance.len, 0, 0); + pass.draw(4, app.cube.instance.len, 4, 0); + pass.draw(4, app.cube.instance.len, 8, 0); + pass.draw(4, app.cube.instance.len, 12, 0); + pass.draw(4, app.cube.instance.len, 16, 0); + pass.draw(4, app.cube.instance.len, 20, 0); + + // light source + pass.setPipeline(app.light.pipeline); + pass.setBindGroup(0, app.camera.bind_group, &.{}); + pass.setBindGroup(1, app.light.bind_group, &.{}); + pass.setVertexBuffer(0, app.cube.mesh.buffer, 0, app.cube.mesh.size); + pass.draw(4, 1, 0, 0); + pass.draw(4, 1, 4, 0); + pass.draw(4, 1, 8, 0); + pass.draw(4, 1, 12, 0); + pass.draw(4, 1, 16, 0); + pass.draw(4, 1, 20, 0); + + pass.end(); + + var command = encoder.finish(null); + defer command.release(); + + queue.submit(&[_]*gpu.CommandBuffer{command}); + core.swap_chain.present(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Gen Texture Light [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} + +const Camera = struct { + const Self = @This(); + + eye: Vec, + target: Vec, + up: Vec, + aspect: f32, + fovy: f32, + near: f32, + far: f32, + bind_group: *gpu.BindGroup, + buffer: Buffer, + + const Uniform = extern struct { + pos: Vec, + mat: Mat, + }; + + fn init(device: *gpu.Device, eye: Vec, target: Vec, up: Vec, aspect: f32, fovy: f32, near: f32, far: f32) Self { + var self: Self = .{ + .eye = eye, + .target = target, + .up = up, + .aspect = aspect, + .near = near, + .far = far, + .fovy = fovy, + .buffer = undefined, + .bind_group = undefined, + }; + + const view = self.buildViewProjMatrix(); + + const uniform = Uniform{ + .pos = self.eye, + .mat = view, + }; + + const buffer = .{ + .buffer = initBuffer(device, .{ .uniform = true }, &@as([20]f32, @bitCast(uniform))), + .size = @sizeOf(@TypeOf(uniform)), + }; + + const layout = Self.bindGroupLayout(device); + const bind_group = device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{ + .layout = layout, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, buffer.buffer, 0, buffer.size), + }, + })); + layout.release(); + + self.buffer = buffer; + self.bind_group = bind_group; + + return self; + } + + fn deinit(self: *Self) void { + self.bind_group.release(); + self.buffer.release(); + } + + fn update(self: *Self, queue: *gpu.Queue) void { + const mat = self.buildViewProjMatrix(); + const uniform = .{ + .pos = self.eye, + .mat = mat, + }; + + queue.writeBuffer(self.buffer.buffer, 0, &[_]Uniform{uniform}); + } + + inline fn buildViewProjMatrix(s: *const Camera) Mat { + const view = zm.lookAtRh(s.eye, s.target, s.up); + const proj = zm.perspectiveFovRh(s.fovy, s.aspect, s.near, s.far); + return zm.mul(view, proj); + } + + inline fn bindGroupLayout(device: *gpu.Device) *gpu.BindGroupLayout { + const visibility = .{ .vertex = true, .fragment = true }; + return device.createBindGroupLayout(&gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &.{ + gpu.BindGroupLayout.Entry.buffer(0, visibility, .uniform, false, 0), + }, + })); + } +}; + +const Buffer = struct { + const Self = @This(); + + buffer: *gpu.Buffer, + size: usize, + len: u32 = 0, + + fn release(self: *Self) void { + self.buffer.release(); + } +}; + +const Cube = struct { + const Self = @This(); + + pipeline: *gpu.RenderPipeline, + mesh: Buffer, + instance: Buffer, + texture: Texture, + + const IPR = 20; // instances per row + const SPACING = 2; // spacing between cubes + const DISPLACEMENT = vec3u(IPR * SPACING / 2, 0, IPR * SPACING / 2); + + fn init() Self { + const device = core.device; + + const texture = Brick.texture(device); + + // instance buffer + var ibuf: [IPR * IPR * 16]f32 = undefined; + + var z: usize = 0; + while (z < IPR) : (z += 1) { + var x: usize = 0; + while (x < IPR) : (x += 1) { + const pos = vec3u(x * SPACING, 0, z * SPACING) - DISPLACEMENT; + const rot = blk: { + if (pos[0] == 0 and pos[2] == 0) { + break :blk zm.rotationZ(0.0); + } else { + break :blk zm.mul(zm.rotationX(zm.clamp(zm.abs(pos[0]), 0, 45.0)), zm.rotationZ(zm.clamp(zm.abs(pos[2]), 0, 45.0))); + } + }; + const index = z * IPR + x; + const inst = Instance{ + .position = pos, + .rotation = rot, + }; + zm.storeMat(ibuf[index * 16 ..], inst.toMat()); + } + } + + const instance = Buffer{ + .buffer = initBuffer(device, .{ .vertex = true }, &ibuf), + .len = IPR * IPR, + .size = @sizeOf(@TypeOf(ibuf)), + }; + + return Self{ + .mesh = mesh(device), + .texture = texture, + .instance = instance, + .pipeline = pipeline(), + }; + } + + fn deinit(self: *Self) void { + self.pipeline.release(); + self.mesh.release(); + self.instance.release(); + self.texture.release(); + } + + fn pipeline() *gpu.RenderPipeline { + const device = core.device; + + const camera_layout = Camera.bindGroupLayout(device); + const texture_layout = Texture.bindGroupLayout(device); + const light_layout = Light.bindGroupLayout(device); + const layout_descriptor = gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &.{ + camera_layout, + texture_layout, + light_layout, + }, + }); + defer camera_layout.release(); + defer texture_layout.release(); + defer light_layout.release(); + + const layout = device.createPipelineLayout(&layout_descriptor); + defer layout.release(); + + const shader = device.createShaderModuleWGSL("cube.wgsl", @embedFile("cube.wgsl")); + defer shader.release(); + + const blend = gpu.BlendState{}; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + }; + + const fragment = gpu.FragmentState.init(.{ + .module = shader, + .entry_point = "fs_main", + .targets = &.{color_target}, + }); + + const descriptor = gpu.RenderPipeline.Descriptor{ + .layout = layout, + .fragment = &fragment, + .vertex = gpu.VertexState.init(.{ + .module = shader, + .entry_point = "vs_main", + .buffers = &.{ + Self.vertexBufferLayout(), + Self.instanceLayout(), + }, + }), + .depth_stencil = &.{ + .format = Texture.DEPTH_FORMAT, + .depth_write_enabled = .true, + .depth_compare = .less, + }, + .primitive = .{ + .cull_mode = .back, + .topology = .triangle_strip, + }, + }; + + return device.createRenderPipeline(&descriptor); + } + + fn mesh(device: *gpu.Device) Buffer { + // generated texture has aspect ratio of 1:2 + // `h` reflects that ratio + // `v` sets how many times texture repeats across surface + const v = 2; + const h = v * 2; + const buf = asFloats(.{ + // z+ face + 0, 0, 1, 0, 0, 1, 0, h, + 1, 0, 1, 0, 0, 1, v, h, + 0, 1, 1, 0, 0, 1, 0, 0, + 1, 1, 1, 0, 0, 1, v, 0, + // z- face + 1, 0, 0, 0, 0, -1, 0, h, + 0, 0, 0, 0, 0, -1, v, h, + 1, 1, 0, 0, 0, -1, 0, 0, + 0, 1, 0, 0, 0, -1, v, 0, + // x+ face + 1, 0, 1, 1, 0, 0, 0, h, + 1, 0, 0, 1, 0, 0, v, h, + 1, 1, 1, 1, 0, 0, 0, 0, + 1, 1, 0, 1, 0, 0, v, 0, + // x- face + 0, 0, 0, -1, 0, 0, 0, h, + 0, 0, 1, -1, 0, 0, v, h, + 0, 1, 0, -1, 0, 0, 0, 0, + 0, 1, 1, -1, 0, 0, v, 0, + // y+ face + 1, 1, 0, 0, 1, 0, 0, h, + 0, 1, 0, 0, 1, 0, v, h, + 1, 1, 1, 0, 1, 0, 0, 0, + 0, 1, 1, 0, 1, 0, v, 0, + // y- face + 0, 0, 0, 0, -1, 0, 0, h, + 1, 0, 0, 0, -1, 0, v, h, + 0, 0, 1, 0, -1, 0, 0, 0, + 1, 0, 1, 0, -1, 0, v, 0, + }); + + return Buffer{ + .buffer = initBuffer(device, .{ .vertex = true }, &buf), + .size = @sizeOf(@TypeOf(buf)), + }; + } + + fn vertexBufferLayout() gpu.VertexBufferLayout { + const attributes = [_]gpu.VertexAttribute{ + .{ + .format = .float32x3, + .offset = 0, + .shader_location = 0, + }, + .{ + .format = .float32x3, + .offset = @sizeOf([3]f32), + .shader_location = 1, + }, + .{ + .format = .float32x2, + .offset = @sizeOf([6]f32), + .shader_location = 2, + }, + }; + return gpu.VertexBufferLayout.init(.{ + .array_stride = @sizeOf([8]f32), + .attributes = &attributes, + }); + } + + fn instanceLayout() gpu.VertexBufferLayout { + const attributes = [_]gpu.VertexAttribute{ + .{ + .format = .float32x4, + .offset = 0, + .shader_location = 3, + }, + .{ + .format = .float32x4, + .offset = @sizeOf([4]f32), + .shader_location = 4, + }, + .{ + .format = .float32x4, + .offset = @sizeOf([8]f32), + .shader_location = 5, + }, + .{ + .format = .float32x4, + .offset = @sizeOf([12]f32), + .shader_location = 6, + }, + }; + + return gpu.VertexBufferLayout.init(.{ + .array_stride = @sizeOf([16]f32), + .step_mode = .instance, + .attributes = &attributes, + }); + } +}; + +fn asFloats(comptime arr: anytype) [arr.len]f32 { + const len = arr.len; + comptime var out: [len]f32 = undefined; + comptime var i = 0; + inline while (i < len) : (i += 1) { + out[i] = @as(f32, @floatFromInt(arr[i])); + } + return out; +} + +const Brick = struct { + const W = 12; + const H = 6; + + fn texture(device: *gpu.Device) Texture { + const slice: []const u8 = &data(); + return Texture.fromData(device, W, H, u8, slice); + } + + fn data() [W * H * 4]u8 { + comptime var out: [W * H * 4]u8 = undefined; + + // fill all the texture with brick color + comptime var i = 0; + inline while (i < H) : (i += 1) { + comptime var j = 0; + inline while (j < W * 4) : (j += 4) { + out[i * W * 4 + j + 0] = 210; + out[i * W * 4 + j + 1] = 30; + out[i * W * 4 + j + 2] = 30; + out[i * W * 4 + j + 3] = 0; + } + } + + const f = 10; + + // fill the cement lines + inline for ([_]comptime_int{ 0, 1 }) |k| { + inline for ([_]comptime_int{ 5 * 4, 11 * 4 }) |m| { + out[k * W * 4 + m + 0] = f; + out[k * W * 4 + m + 1] = f; + out[k * W * 4 + m + 2] = f; + out[k * W * 4 + m + 3] = 0; + } + } + + inline for ([_]comptime_int{ 3, 4 }) |k| { + inline for ([_]comptime_int{ 2 * 4, 8 * 4 }) |m| { + out[k * W * 4 + m + 0] = f; + out[k * W * 4 + m + 1] = f; + out[k * W * 4 + m + 2] = f; + out[k * W * 4 + m + 3] = 0; + } + } + + inline for ([_]comptime_int{ 2, 5 }) |k| { + comptime var m = 0; + inline while (m < W * 4) : (m += 4) { + out[k * W * 4 + m + 0] = f; + out[k * W * 4 + m + 1] = f; + out[k * W * 4 + m + 2] = f; + out[k * W * 4 + m + 3] = 0; + } + } + + return out; + } +}; + +// don't confuse with gpu.Texture +const Texture = struct { + const Self = @This(); + + texture: *gpu.Texture, + view: *gpu.TextureView, + sampler: *gpu.Sampler, + bind_group: ?*gpu.BindGroup, + + const DEPTH_FORMAT = .depth32_float; + const FORMAT = .rgba8_unorm; + + fn release(self: *Self) void { + self.texture.release(); + self.view.release(); + self.sampler.release(); + if (self.bind_group) |bind_group| bind_group.release(); + } + + fn fromData(device: *gpu.Device, width: u32, height: u32, comptime T: type, data: []const T) Self { + const extent = gpu.Extent3D{ + .width = width, + .height = height, + }; + + const texture = device.createTexture(&gpu.Texture.Descriptor{ + .size = extent, + .format = FORMAT, + .usage = .{ .copy_dst = true, .texture_binding = true }, + }); + + const view = texture.createView(&gpu.TextureView.Descriptor{ + .format = FORMAT, + .dimension = .dimension_2d, + .array_layer_count = 1, + .mip_level_count = 1, + }); + + const sampler = device.createSampler(&gpu.Sampler.Descriptor{ + .address_mode_u = .repeat, + .address_mode_v = .repeat, + .address_mode_w = .repeat, + .mag_filter = .linear, + .min_filter = .linear, + .mipmap_filter = .linear, + .max_anisotropy = 1, // 1,2,4,8,16 + }); + + core.queue.writeTexture( + &gpu.ImageCopyTexture{ + .texture = texture, + }, + &gpu.Texture.DataLayout{ + .bytes_per_row = 4 * width, + .rows_per_image = height, + }, + &extent, + data, + ); + + const bind_group_layout = Self.bindGroupLayout(device); + const bind_group = device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{ + .layout = bind_group_layout, + .entries = &.{ + gpu.BindGroup.Entry.textureView(0, view), + gpu.BindGroup.Entry.sampler(1, sampler), + }, + })); + bind_group_layout.release(); + + return Self{ + .view = view, + .texture = texture, + .sampler = sampler, + .bind_group = bind_group, + }; + } + + fn depth(device: *gpu.Device, width: u32, height: u32) Self { + const extent = gpu.Extent3D{ + .width = width, + .height = height, + }; + + const texture = device.createTexture(&gpu.Texture.Descriptor{ + .size = extent, + .format = DEPTH_FORMAT, + .usage = .{ + .render_attachment = true, + .texture_binding = true, + }, + }); + + const view = texture.createView(&gpu.TextureView.Descriptor{ + .dimension = .dimension_2d, + .array_layer_count = 1, + .mip_level_count = 1, + }); + + const sampler = device.createSampler(&gpu.Sampler.Descriptor{ + .mag_filter = .linear, + .compare = .less_equal, + }); + + return Self{ + .texture = texture, + .view = view, + .sampler = sampler, + .bind_group = null, // not used + }; + } + + inline fn bindGroupLayout(device: *gpu.Device) *gpu.BindGroupLayout { + const visibility = .{ .fragment = true }; + const Entry = gpu.BindGroupLayout.Entry; + return device.createBindGroupLayout(&gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &.{ + Entry.texture(0, visibility, .float, .dimension_2d, false), + Entry.sampler(1, visibility, .filtering), + }, + })); + } +}; + +const Light = struct { + const Self = @This(); + + uniform: Uniform, + buffer: Buffer, + bind_group: *gpu.BindGroup, + pipeline: *gpu.RenderPipeline, + + const Uniform = extern struct { + position: Vec, + color: Vec, + }; + + fn init() Self { + const device = core.device; + const uniform = Uniform{ + .color = vec3u(1, 1, 1), + .position = vec3u(3, 7, 2), + }; + + const buffer = .{ + .buffer = initBuffer(device, .{ .uniform = true }, &@as([8]f32, @bitCast(uniform))), + .size = @sizeOf(@TypeOf(uniform)), + }; + + const layout = Self.bindGroupLayout(device); + const bind_group = device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{ + .layout = layout, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, buffer.buffer, 0, buffer.size), + }, + })); + layout.release(); + + return Self{ + .buffer = buffer, + .uniform = uniform, + .bind_group = bind_group, + .pipeline = Self.pipeline(), + }; + } + + fn deinit(self: *Self) void { + self.buffer.release(); + self.bind_group.release(); + self.pipeline.release(); + } + + fn update(self: *Self, queue: *gpu.Queue, delta: f32) void { + const old = self.uniform; + const new = Light.Uniform{ + .position = zm.qmul(zm.quatFromAxisAngle(vec3u(0, 1, 0), delta), old.position), + .color = old.color, + }; + queue.writeBuffer(self.buffer.buffer, 0, &[_]Light.Uniform{new}); + self.uniform = new; + } + + inline fn bindGroupLayout(device: *gpu.Device) *gpu.BindGroupLayout { + const visibility = .{ .vertex = true, .fragment = true }; + const Entry = gpu.BindGroupLayout.Entry; + return device.createBindGroupLayout(&gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &.{ + Entry.buffer(0, visibility, .uniform, false, 0), + }, + })); + } + + fn pipeline() *gpu.RenderPipeline { + const device = core.device; + + const camera_layout = Camera.bindGroupLayout(device); + const light_layout = Light.bindGroupLayout(device); + const layout_descriptor = gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &.{ + camera_layout, + light_layout, + }, + }); + defer camera_layout.release(); + defer light_layout.release(); + + const layout = device.createPipelineLayout(&layout_descriptor); + defer layout.release(); + + const shader = core.device.createShaderModuleWGSL("light.wgsl", @embedFile("light.wgsl")); + defer shader.release(); + + const blend = gpu.BlendState{}; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + }; + + const fragment = gpu.FragmentState.init(.{ + .module = shader, + .entry_point = "fs_main", + .targets = &.{color_target}, + }); + + const descriptor = gpu.RenderPipeline.Descriptor{ + .layout = layout, + .fragment = &fragment, + .vertex = gpu.VertexState.init(.{ + .module = shader, + .entry_point = "vs_main", + .buffers = &.{ + Cube.vertexBufferLayout(), + }, + }), + .depth_stencil = &.{ + .format = Texture.DEPTH_FORMAT, + .depth_write_enabled = .true, + .depth_compare = .less, + }, + .primitive = .{ + .cull_mode = .back, + .topology = .triangle_strip, + }, + }; + + return device.createRenderPipeline(&descriptor); + } +}; + +inline fn initBuffer(device: *gpu.Device, usage: gpu.Buffer.UsageFlags, data: anytype) *gpu.Buffer { + std.debug.assert(@typeInfo(@TypeOf(data)) == .Pointer); + const T = std.meta.Elem(@TypeOf(data)); + + var u = usage; + u.copy_dst = true; + const buffer = device.createBuffer(&.{ + .size = @sizeOf(T) * data.len, + .usage = u, + .mapped_at_creation = .true, + }); + + const mapped = buffer.getMappedRange(T, 0, data.len); + @memcpy(mapped.?, data); + buffer.unmap(); + return buffer; +} + +fn vec3i(x: isize, y: isize, z: isize) Vec { + return Vec{ @floatFromInt(x), @floatFromInt(y), @floatFromInt(z), 0.0 }; +} + +fn vec3u(x: usize, y: usize, z: usize) Vec { + return zm.Vec{ @floatFromInt(x), @floatFromInt(y), @floatFromInt(z), 0.0 }; +} + +// todo indside Cube +const Instance = struct { + const Self = @This(); + + position: Vec, + rotation: Mat, + + fn toMat(self: *const Self) Mat { + return zm.mul(self.rotation, zm.translationV(self.position)); + } +}; diff --git a/src/core/examples/image-blur/blur.wgsl b/src/core/examples/image-blur/blur.wgsl new file mode 100644 index 00000000..e8c5d1be --- /dev/null +++ b/src/core/examples/image-blur/blur.wgsl @@ -0,0 +1,81 @@ +struct Params { + filterDim : i32, + blockDim : u32, +} + +@group(0) @binding(0) var samp : sampler; +@group(0) @binding(1) var params : Params; +@group(1) @binding(1) var inputTex : texture_2d; +@group(1) @binding(2) var outputTex : texture_storage_2d; + +struct Flip { + value : u32, +} +@group(1) @binding(3) var flip : Flip; + +// This shader blurs the input texture in one direction, depending on whether +// |flip.value| is 0 or 1. +// It does so by running (128 / 4) threads per workgroup to load 128 +// texels into 4 rows of shared memory. Each thread loads a +// 4 x 4 block of texels to take advantage of the texture sampling +// hardware. +// Then, each thread computes the blur result by averaging the adjacent texel values +// in shared memory. +// Because we're operating on a subset of the texture, we cannot compute all of the +// results since not all of the neighbors are available in shared memory. +// Specifically, with 128 x 128 tiles, we can only compute and write out +// square blocks of size 128 - (filterSize - 1). We compute the number of blocks +// needed in Javascript and dispatch that amount. + +var tile : array, 128>, 4>; + +@compute @workgroup_size(32, 1, 1) +fn main( + @builtin(workgroup_id) WorkGroupID : vec3, + @builtin(local_invocation_id) LocalInvocationID : vec3 +) { + let filterOffset = (params.filterDim - 1) / 2; + let dims = vec2(textureDimensions(inputTex, 0)); + let baseIndex = vec2(WorkGroupID.xy * vec2(params.blockDim, 4) + + LocalInvocationID.xy * vec2(4, 1)) + - vec2(filterOffset, 0); + + for (var r = 0; r < 4; r++) { + for (var c = 0; c < 4; c++) { + var loadIndex = baseIndex + vec2(c, r); + if (flip.value != 0u) { + loadIndex = loadIndex.yx; + } + + tile[r][4 * LocalInvocationID.x + u32(c)] = textureSampleLevel( + inputTex, + samp, + (vec2(loadIndex) + vec2(0.25, 0.25)) / vec2(dims), + 0.0 + ).rgb; + } + } + + workgroupBarrier(); + + for (var r = 0; r < 4; r++) { + for (var c = 0; c < 4; c++) { + var writeIndex = baseIndex + vec2(c, r); + if (flip.value != 0) { + writeIndex = writeIndex.yx; + } + + let center = i32(4 * LocalInvocationID.x) + c; + if (center >= filterOffset && + center < 128 - filterOffset && + all(writeIndex < dims)) { + var acc = vec3(0.0, 0.0, 0.0); + for (var f = 0; f < params.filterDim; f++) { + var i = center + f - filterOffset; + acc = acc + (1.0 / f32(params.filterDim)) * tile[r][i]; + } + textureStore(outputTex, writeIndex, vec4(acc, 1.0)); + } + } + } +} diff --git a/src/core/examples/image-blur/fullscreen_textured_quad.wgsl b/src/core/examples/image-blur/fullscreen_textured_quad.wgsl new file mode 100644 index 00000000..61c461c0 --- /dev/null +++ b/src/core/examples/image-blur/fullscreen_textured_quad.wgsl @@ -0,0 +1,38 @@ +@group(0) @binding(0) var mySampler : sampler; +@group(0) @binding(1) var myTexture : texture_2d; + +struct VertexOutput { + @builtin(position) Position : vec4, + @location(0) fragUV : vec2, +} + +@vertex +fn vert_main(@builtin(vertex_index) VertexIndex : u32) -> VertexOutput { + var pos = array, 6>( + vec2( 1.0, 1.0), + vec2( 1.0, -1.0), + vec2(-1.0, -1.0), + vec2( 1.0, 1.0), + vec2(-1.0, -1.0), + vec2(-1.0, 1.0) + ); + + var uv = array, 6>( + vec2(1.0, 0.0), + vec2(1.0, 1.0), + vec2(0.0, 1.0), + vec2(1.0, 0.0), + vec2(0.0, 1.0), + vec2(0.0, 0.0) + ); + + var output : VertexOutput; + output.Position = vec4(pos[VertexIndex], 0.0, 1.0); + output.fragUV = uv[VertexIndex]; + return output; +} + +@fragment +fn frag_main(@location(0) fragUV : vec2) -> @location(0) vec4 { + return textureSample(myTexture, mySampler, fragUV); +} diff --git a/src/core/examples/image-blur/main.zig b/src/core/examples/image-blur/main.zig new file mode 100644 index 00000000..2d39a48a --- /dev/null +++ b/src/core/examples/image-blur/main.zig @@ -0,0 +1,326 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const zigimg = @import("zigimg"); +const assets = @import("assets"); + +title_timer: core.Timer, +blur_pipeline: *gpu.ComputePipeline, +fullscreen_quad_pipeline: *gpu.RenderPipeline, +cube_texture: *gpu.Texture, +textures: [2]*gpu.Texture, +blur_params_buffer: *gpu.Buffer, +compute_constants: *gpu.BindGroup, +compute_bind_group_0: *gpu.BindGroup, +compute_bind_group_1: *gpu.BindGroup, +compute_bind_group_2: *gpu.BindGroup, +show_result_bind_group: *gpu.BindGroup, +img_size: gpu.Extent3D, + +pub const App = @This(); + +// Constants from the blur.wgsl shader +const tile_dimension: u32 = 128; +const batch: [2]u32 = .{ 4, 4 }; + +// Currently hardcoded +const filter_size: u32 = 15; +const iterations: u32 = 2; +var block_dimension: u32 = tile_dimension - (filter_size - 1); +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +pub fn init(app: *App) !void { + try core.init(.{}); + const allocator = gpa.allocator(); + + const queue = core.queue; + + const blur_shader_module = core.device.createShaderModuleWGSL("blur.wgsl", @embedFile("blur.wgsl")); + + const blur_pipeline_descriptor = gpu.ComputePipeline.Descriptor{ + .compute = gpu.ProgrammableStageDescriptor{ + .module = blur_shader_module, + .entry_point = "main", + }, + }; + + const blur_pipeline = core.device.createComputePipeline(&blur_pipeline_descriptor); + blur_shader_module.release(); + + const fullscreen_quad_vs_module = core.device.createShaderModuleWGSL( + "fullscreen_textured_quad.wgsl", + @embedFile("fullscreen_textured_quad.wgsl"), + ); + + const fullscreen_quad_fs_module = core.device.createShaderModuleWGSL( + "fullscreen_textured_quad.wgsl", + @embedFile("fullscreen_textured_quad.wgsl"), + ); + + const blend = gpu.BlendState{}; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + + const fragment_state = gpu.FragmentState.init(.{ + .module = fullscreen_quad_fs_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + + const fullscreen_quad_pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment_state, + .vertex = .{ + .module = fullscreen_quad_vs_module, + .entry_point = "vert_main", + }, + }; + + const fullscreen_quad_pipeline = core.device.createRenderPipeline(&fullscreen_quad_pipeline_descriptor); + fullscreen_quad_vs_module.release(); + fullscreen_quad_fs_module.release(); + + const sampler = core.device.createSampler(&.{ + .mag_filter = .linear, + .min_filter = .linear, + }); + + var img = try zigimg.Image.fromMemory(allocator, assets.gotta_go_fast_png); + defer img.deinit(); + + const img_size = gpu.Extent3D{ .width = @as(u32, @intCast(img.width)), .height = @as(u32, @intCast(img.height)) }; + + const cube_texture = core.device.createTexture(&.{ + .size = img_size, + .format = .rgba8_unorm, + .usage = .{ + .texture_binding = true, + .copy_dst = true, + .render_attachment = true, + }, + }); + + const data_layout = gpu.Texture.DataLayout{ + .bytes_per_row = @as(u32, @intCast(img.width * 4)), + .rows_per_image = @as(u32, @intCast(img.height)), + }; + + switch (img.pixels) { + .rgba32 => |pixels| queue.writeTexture(&.{ .texture = cube_texture }, &data_layout, &img_size, pixels), + .rgb24 => |pixels| { + const data = try rgb24ToRgba32(allocator, pixels); + defer data.deinit(allocator); + queue.writeTexture(&.{ .texture = cube_texture }, &data_layout, &img_size, data.rgba32); + }, + else => @panic("unsupported image color format"), + } + + var textures: [2]*gpu.Texture = undefined; + for (textures, 0..) |_, i| { + textures[i] = core.device.createTexture(&.{ + .size = img_size, + .format = .rgba8_unorm, + .usage = .{ + .storage_binding = true, + .texture_binding = true, + .copy_dst = true, + }, + }); + } + + // the shader blurs the input texture in one direction, + // depending on whether flip value is 0 or 1 + var flip: [2]*gpu.Buffer = undefined; + for (flip, 0..) |_, i| { + const buffer = core.device.createBuffer(&.{ + .usage = .{ .uniform = true }, + .size = @sizeOf(u32), + .mapped_at_creation = .true, + }); + + const buffer_mapped = buffer.getMappedRange(u32, 0, 1); + buffer_mapped.?[0] = @as(u32, @intCast(i)); + buffer.unmap(); + + flip[i] = buffer; + } + + const blur_params_buffer = core.device.createBuffer(&.{ + .size = 8, + .usage = .{ .copy_dst = true, .uniform = true }, + }); + + const blur_bind_group_layout0 = blur_pipeline.getBindGroupLayout(0); + const blur_bind_group_layout1 = blur_pipeline.getBindGroupLayout(1); + const fullscreen_bind_group_layout = fullscreen_quad_pipeline.getBindGroupLayout(0); + const cube_texture_view = cube_texture.createView(&gpu.TextureView.Descriptor{}); + const texture0_view = textures[0].createView(&gpu.TextureView.Descriptor{}); + const texture1_view = textures[1].createView(&gpu.TextureView.Descriptor{}); + + const compute_constants = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{ + .layout = blur_bind_group_layout0, + .entries = &.{ + gpu.BindGroup.Entry.sampler(0, sampler), + gpu.BindGroup.Entry.buffer(1, blur_params_buffer, 0, 8), + }, + })); + + const compute_bind_group_0 = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{ + .layout = blur_bind_group_layout1, + .entries = &.{ + gpu.BindGroup.Entry.textureView(1, cube_texture_view), + gpu.BindGroup.Entry.textureView(2, texture0_view), + gpu.BindGroup.Entry.buffer(3, flip[0], 0, 4), + }, + })); + + const compute_bind_group_1 = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{ + .layout = blur_bind_group_layout1, + .entries = &.{ + gpu.BindGroup.Entry.textureView(1, texture0_view), + gpu.BindGroup.Entry.textureView(2, texture1_view), + gpu.BindGroup.Entry.buffer(3, flip[1], 0, 4), + }, + })); + + const compute_bind_group_2 = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{ + .layout = blur_bind_group_layout1, + .entries = &.{ + gpu.BindGroup.Entry.textureView(1, texture1_view), + gpu.BindGroup.Entry.textureView(2, texture0_view), + gpu.BindGroup.Entry.buffer(3, flip[0], 0, 4), + }, + })); + + const show_result_bind_group = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{ + .layout = fullscreen_bind_group_layout, + .entries = &.{ + gpu.BindGroup.Entry.sampler(0, sampler), + gpu.BindGroup.Entry.textureView(1, texture1_view), + }, + })); + + blur_bind_group_layout0.release(); + blur_bind_group_layout1.release(); + fullscreen_bind_group_layout.release(); + sampler.release(); + flip[0].release(); + flip[1].release(); + cube_texture_view.release(); + texture0_view.release(); + texture1_view.release(); + + const blur_params_buffer_data = [_]u32{ filter_size, block_dimension }; + queue.writeBuffer(blur_params_buffer, 0, &blur_params_buffer_data); + + app.title_timer = try core.Timer.start(); + app.blur_pipeline = blur_pipeline; + app.fullscreen_quad_pipeline = fullscreen_quad_pipeline; + app.cube_texture = cube_texture; + app.textures = textures; + app.blur_params_buffer = blur_params_buffer; + app.compute_constants = compute_constants; + app.compute_bind_group_0 = compute_bind_group_0; + app.compute_bind_group_1 = compute_bind_group_1; + app.compute_bind_group_2 = compute_bind_group_2; + app.show_result_bind_group = show_result_bind_group; + app.img_size = img_size; +} + +pub fn deinit(app: *App) void { + defer _ = gpa.deinit(); + defer core.deinit(); + + app.blur_pipeline.release(); + app.fullscreen_quad_pipeline.release(); + app.cube_texture.release(); + app.textures[0].release(); + app.textures[1].release(); + app.blur_params_buffer.release(); + app.compute_constants.release(); + app.compute_bind_group_0.release(); + app.compute_bind_group_1.release(); + app.compute_bind_group_2.release(); + app.show_result_bind_group.release(); +} + +pub fn update(app: *App) !bool { + var iter = core.pollEvents(); + while (iter.next()) |event| { + if (event == .close) return true; + } + + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const encoder = core.device.createCommandEncoder(null); + + const compute_pass = encoder.beginComputePass(null); + compute_pass.setPipeline(app.blur_pipeline); + compute_pass.setBindGroup(0, app.compute_constants, &.{}); + + const width: u32 = @as(u32, @intCast(app.img_size.width)); + const height: u32 = @as(u32, @intCast(app.img_size.height)); + compute_pass.setBindGroup(1, app.compute_bind_group_0, &.{}); + compute_pass.dispatchWorkgroups(try std.math.divCeil(u32, width, block_dimension), try std.math.divCeil(u32, height, batch[1]), 1); + + compute_pass.setBindGroup(1, app.compute_bind_group_1, &.{}); + compute_pass.dispatchWorkgroups(try std.math.divCeil(u32, height, block_dimension), try std.math.divCeil(u32, width, batch[1]), 1); + + var i: u32 = 0; + while (i < iterations - 1) : (i += 1) { + compute_pass.setBindGroup(1, app.compute_bind_group_2, &.{}); + compute_pass.dispatchWorkgroups(try std.math.divCeil(u32, width, block_dimension), try std.math.divCeil(u32, height, batch[1]), 1); + + compute_pass.setBindGroup(1, app.compute_bind_group_1, &.{}); + compute_pass.dispatchWorkgroups(try std.math.divCeil(u32, height, block_dimension), try std.math.divCeil(u32, width, batch[1]), 1); + } + compute_pass.end(); + compute_pass.release(); + + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = std.mem.zeroes(gpu.Color), + .load_op = .clear, + .store_op = .store, + }; + + const render_pass_descriptor = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + }); + + const render_pass = encoder.beginRenderPass(&render_pass_descriptor); + render_pass.setPipeline(app.fullscreen_quad_pipeline); + render_pass.setBindGroup(0, app.show_result_bind_group, &.{}); + render_pass.draw(6, 1, 0, 0); + render_pass.end(); + render_pass.release(); + + var command = encoder.finish(null); + encoder.release(); + const queue = core.queue; + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Image Blur [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} + +fn rgb24ToRgba32(allocator: std.mem.Allocator, in: []zigimg.color.Rgb24) !zigimg.color.PixelStorage { + const out = try zigimg.color.PixelStorage.init(allocator, .rgba32, in.len); + var i: usize = 0; + while (i < in.len) : (i += 1) { + out.rgba32[i] = zigimg.color.Rgba32{ .r = in[i].r, .g = in[i].g, .b = in[i].b, .a = 255 }; + } + return out; +} diff --git a/src/core/examples/image/fullscreen_textured_quad.wgsl b/src/core/examples/image/fullscreen_textured_quad.wgsl new file mode 100644 index 00000000..3238e6a4 --- /dev/null +++ b/src/core/examples/image/fullscreen_textured_quad.wgsl @@ -0,0 +1,39 @@ +@group(0) @binding(0) var mySampler : sampler; +@group(0) @binding(1) var myTexture : texture_2d; + +struct VertexOutput { + @builtin(position) Position : vec4, + @location(0) fragUV : vec2, +} + +@vertex +fn vert_main(@builtin(vertex_index) VertexIndex : u32) -> VertexOutput { + // Draw a fullscreen quad using two triangles, with UV coordinates (normalized pixel coordinates) + // that would have the full texture be displayed. + var pos = array, 6>( + vec2( 1.0, 1.0), // right, top + vec2( 1.0, -1.0), // right, bottom + vec2(-1.0, -1.0), // left, bottom + vec2( 1.0, 1.0), // right, top + vec2(-1.0, -1.0), // left, bottom + vec2(-1.0, 1.0) // left, top + ); + var uv = array, 6>( + vec2(1.0, 0.0), + vec2(1.0, 1.0), + vec2(0.0, 1.0), + vec2(1.0, 0.0), + vec2(0.0, 1.0), + vec2(0.0, 0.0) + ); + + var output : VertexOutput; + output.Position = vec4(pos[VertexIndex], 0.0, 1.0); + output.fragUV = uv[VertexIndex]; + return output; +} + +@fragment +fn frag_main(@location(0) fragUV : vec2) -> @location(0) vec4 { + return textureSample(myTexture, mySampler, fragUV); +} diff --git a/src/core/examples/image/main.zig b/src/core/examples/image/main.zig new file mode 100644 index 00000000..fafe1b75 --- /dev/null +++ b/src/core/examples/image/main.zig @@ -0,0 +1,184 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const zigimg = @import("zigimg"); +const assets = @import("assets"); + +title_timer: core.Timer, +pipeline: *gpu.RenderPipeline, +texture: *gpu.Texture, +bind_group: *gpu.BindGroup, +img_size: gpu.Extent3D, + +pub const App = @This(); + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +pub fn init(app: *App) !void { + try core.init(.{}); + const allocator = gpa.allocator(); + + // Load our shader that will render a fullscreen textured quad using two triangles, needed to + // get the image on screen. + const fullscreen_quad_vs_module = core.device.createShaderModuleWGSL( + "fullscreen_textured_quad.wgsl", + @embedFile("fullscreen_textured_quad.wgsl"), + ); + defer fullscreen_quad_vs_module.release(); + const fullscreen_quad_fs_module = core.device.createShaderModuleWGSL( + "fullscreen_textured_quad.wgsl", + @embedFile("fullscreen_textured_quad.wgsl"), + ); + defer fullscreen_quad_fs_module.release(); + + // Create our render pipeline + const blend = gpu.BlendState{}; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment_state = gpu.FragmentState.init(.{ + .module = fullscreen_quad_fs_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment_state, + .vertex = .{ + .module = fullscreen_quad_vs_module, + .entry_point = "vert_main", + }, + }; + const pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + + // Create a texture sampler. This determines what happens when the texture doesn't match the + // dimensions of the screen it's being displayed on. If the image needs to be magnified or + // minified to fit, it can be linearly interpolated (i.e. 'blurred', .linear) or the nearest + // pixel may be used (i.e. 'pixelated', .nearest) + const sampler = core.device.createSampler(&.{ + .mag_filter = .linear, + .min_filter = .linear, + }); + defer sampler.release(); + + // Load the pixels of the image + var img = try zigimg.Image.fromMemory(allocator, assets.gotta_go_fast_png); + defer img.deinit(); + const img_size = gpu.Extent3D{ .width = @as(u32, @intCast(img.width)), .height = @as(u32, @intCast(img.height)) }; + + // Create a texture + const texture = core.device.createTexture(&.{ + .size = img_size, + .format = .rgba8_unorm, + .usage = .{ + .texture_binding = true, + .copy_dst = true, + .render_attachment = true, + }, + }); + + // Upload the pixels (from the CPU) to the GPU. You could e.g. do this once per frame if you + // wanted the image to be updated dynamically. + const data_layout = gpu.Texture.DataLayout{ + .bytes_per_row = @as(u32, @intCast(img.width * 4)), + .rows_per_image = @as(u32, @intCast(img.height)), + }; + switch (img.pixels) { + .rgba32 => |pixels| core.queue.writeTexture(&.{ .texture = texture }, &data_layout, &img_size, pixels), + .rgb24 => |pixels| { + const data = try rgb24ToRgba32(allocator, pixels); + defer data.deinit(allocator); + core.queue.writeTexture(&.{ .texture = texture }, &data_layout, &img_size, data.rgba32); + }, + else => @panic("unsupported image color format"), + } + + // Describe which data we will pass to our shader (GPU program) + const bind_group_layout = pipeline.getBindGroupLayout(0); + defer bind_group_layout.release(); + const texture_view = texture.createView(&gpu.TextureView.Descriptor{}); + defer texture_view.release(); + const bind_group = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{ + .layout = bind_group_layout, + .entries = &.{ + gpu.BindGroup.Entry.sampler(0, sampler), + gpu.BindGroup.Entry.textureView(1, texture_view), + }, + })); + + app.* = .{ + .title_timer = try core.Timer.start(), + .pipeline = pipeline, + .texture = texture, + .bind_group = bind_group, + .img_size = img_size, + }; +} + +pub fn deinit(app: *App) void { + defer _ = gpa.deinit(); + defer core.deinit(); + app.pipeline.release(); + app.texture.release(); + app.bind_group.release(); +} + +pub fn update(app: *App) !bool { + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + defer back_buffer_view.release(); + + // Poll for events (keyboard input, etc.) + var iter = core.pollEvents(); + while (iter.next()) |event| { + if (event == .close) return true; + } + + const encoder = core.device.createCommandEncoder(null); + defer encoder.release(); + + // Begin our render pass by clearing the pixels that were on the screen from the previous frame. + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = std.mem.zeroes(gpu.Color), + .load_op = .clear, + .store_op = .store, + }; + const render_pass_descriptor = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + }); + const render_pass = encoder.beginRenderPass(&render_pass_descriptor); + defer render_pass.release(); + + // Render using our pipeline + render_pass.setPipeline(app.pipeline); + render_pass.setBindGroup(0, app.bind_group, &.{}); + render_pass.draw(6, 1, 0, 0); // Tell the GPU to draw 6 vertices, one object + render_pass.end(); + + // Submit all the commands to the GPU and render the frame. + var command = encoder.finish(null); + defer command.release(); + core.queue.submit(&[_]*gpu.CommandBuffer{command}); + core.swap_chain.present(); + + // update the window title every second to have the FPS + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Image [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} + +fn rgb24ToRgba32(allocator: std.mem.Allocator, in: []zigimg.color.Rgb24) !zigimg.color.PixelStorage { + const out = try zigimg.color.PixelStorage.init(allocator, .rgba32, in.len); + var i: usize = 0; + while (i < in.len) : (i += 1) { + out.rgba32[i] = zigimg.color.Rgba32{ .r = in[i].r, .g = in[i].g, .b = in[i].b, .a = 255 }; + } + return out; +} diff --git a/src/core/examples/instanced-cube/cube_mesh.zig b/src/core/examples/instanced-cube/cube_mesh.zig new file mode 100644 index 00000000..f26c75ac --- /dev/null +++ b/src/core/examples/instanced-cube/cube_mesh.zig @@ -0,0 +1,49 @@ +pub const Vertex = extern struct { + pos: @Vector(4, f32), + col: @Vector(4, f32), + uv: @Vector(2, f32), +}; + +pub const vertices = [_]Vertex{ + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, +}; diff --git a/src/core/examples/instanced-cube/main.zig b/src/core/examples/instanced-cube/main.zig new file mode 100755 index 00000000..f0babde7 --- /dev/null +++ b/src/core/examples/instanced-cube/main.zig @@ -0,0 +1,205 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const zm = @import("zmath"); +const Vertex = @import("cube_mesh.zig").Vertex; +const vertices = @import("cube_mesh.zig").vertices; + +const UniformBufferObject = struct { + mat: zm.Mat, +}; + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +title_timer: core.Timer, + +timer: core.Timer, +pipeline: *gpu.RenderPipeline, +vertex_buffer: *gpu.Buffer, +uniform_buffer: *gpu.Buffer, +bind_group: *gpu.BindGroup, + +pub const App = @This(); + +pub fn init(app: *App) !void { + try core.init(.{}); + app.timer = try core.Timer.start(); + + const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); + + const vertex_attributes = [_]gpu.VertexAttribute{ + .{ .format = .float32x4, .offset = @offsetOf(Vertex, "pos"), .shader_location = 0 }, + .{ .format = .float32x2, .offset = @offsetOf(Vertex, "uv"), .shader_location = 1 }, + }; + const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{ + .array_stride = @sizeOf(Vertex), + .step_mode = .vertex, + .attributes = &vertex_attributes, + }); + + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + + const bgle = gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, true, 0); + const bgl = core.device.createBindGroupLayout( + &gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &.{bgle}, + }), + ); + + const bind_group_layouts = [_]*gpu.BindGroupLayout{bgl}; + const pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &bind_group_layouts, + })); + + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + .layout = pipeline_layout, + .vertex = gpu.VertexState.init(.{ + .module = shader_module, + .entry_point = "vertex_main", + .buffers = &.{vertex_buffer_layout}, + }), + .primitive = .{ + .cull_mode = .back, + }, + }; + + const vertex_buffer = core.device.createBuffer(&.{ + .usage = .{ .vertex = true }, + .size = @sizeOf(Vertex) * vertices.len, + .mapped_at_creation = .true, + }); + const vertex_mapped = vertex_buffer.getMappedRange(Vertex, 0, vertices.len); + @memcpy(vertex_mapped.?, vertices[0..]); + vertex_buffer.unmap(); + + const x_count = 4; + const y_count = 4; + const num_instances = x_count * y_count; + + const uniform_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = @sizeOf(UniformBufferObject) * num_instances, + .mapped_at_creation = .false, + }); + const bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bgl, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, uniform_buffer, 0, @sizeOf(UniformBufferObject) * num_instances), + }, + }), + ); + + app.title_timer = try core.Timer.start(); + app.pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + app.vertex_buffer = vertex_buffer; + app.uniform_buffer = uniform_buffer; + app.bind_group = bind_group; + + shader_module.release(); + pipeline_layout.release(); + bgl.release(); +} + +pub fn deinit(app: *App) void { + defer _ = gpa.deinit(); + defer core.deinit(); + + app.pipeline.release(); + app.vertex_buffer.release(); + app.bind_group.release(); + app.uniform_buffer.release(); +} + +pub fn update(app: *App) !bool { + var iter = core.pollEvents(); + while (iter.next()) |event| { + switch (event) { + .key_press => |ev| { + if (ev.key == .space) return true; + }, + .close => return true, + else => {}, + } + } + + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = std.mem.zeroes(gpu.Color), + .load_op = .clear, + .store_op = .store, + }; + + const encoder = core.device.createCommandEncoder(null); + const render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + }); + + { + const proj = zm.perspectiveFovRh( + (std.math.pi / 3.0), + @as(f32, @floatFromInt(core.descriptor.width)) / @as(f32, @floatFromInt(core.descriptor.height)), + 10, + 30, + ); + + var ubos: [16]UniformBufferObject = undefined; + const time = app.timer.read(); + const step: f32 = 4.0; + var m: u8 = 0; + var x: u8 = 0; + while (x < 4) : (x += 1) { + var y: u8 = 0; + while (y < 4) : (y += 1) { + const trans = zm.translation(step * (@as(f32, @floatFromInt(x)) - 2.0 + 0.5), step * (@as(f32, @floatFromInt(y)) - 2.0 + 0.5), -20); + const localTime = time + @as(f32, @floatFromInt(m)) * 0.5; + const model = zm.mul(zm.mul(zm.mul(zm.rotationX(localTime * (std.math.pi / 2.1)), zm.rotationY(localTime * (std.math.pi / 0.9))), zm.rotationZ(localTime * (std.math.pi / 1.3))), trans); + const mvp = zm.mul(model, proj); + const ubo = UniformBufferObject{ + .mat = mvp, + }; + ubos[m] = ubo; + m += 1; + } + } + encoder.writeBuffer(app.uniform_buffer, 0, &ubos); + } + + const pass = encoder.beginRenderPass(&render_pass_info); + pass.setPipeline(app.pipeline); + pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len); + pass.setBindGroup(0, app.bind_group, &.{0}); + pass.draw(vertices.len, 16, 0, 0); + pass.end(); + pass.release(); + + var command = encoder.finish(null); + encoder.release(); + + const queue = core.queue; + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Instanced Cube [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} diff --git a/src/core/examples/instanced-cube/shader.wgsl b/src/core/examples/instanced-cube/shader.wgsl new file mode 100644 index 00000000..1e279d8d --- /dev/null +++ b/src/core/examples/instanced-cube/shader.wgsl @@ -0,0 +1,25 @@ +@binding(0) @group(0) var ubos : array, 16>; + +struct VertexOutput { + @builtin(position) position_clip : vec4, + @location(0) fragUV : vec2, + @location(1) fragPosition: vec4, +}; + +@vertex +fn vertex_main(@builtin(instance_index) instanceIdx : u32, + @location(0) position : vec4, + @location(1) uv : vec2) -> VertexOutput { + var output : VertexOutput; + output.position_clip = ubos[instanceIdx] * position; + output.fragUV = uv; + output.fragPosition = 0.5 * (position + vec4(1.0, 1.0, 1.0, 1.0)); + return output; +} + +@fragment fn frag_main( + @location(0) fragUV: vec2, + @location(1) fragPosition: vec4 +) -> @location(0) vec4 { + return fragPosition; +} \ No newline at end of file diff --git a/src/core/examples/map-async/main.wgsl b/src/core/examples/map-async/main.wgsl new file mode 100644 index 00000000..a4270992 --- /dev/null +++ b/src/core/examples/map-async/main.wgsl @@ -0,0 +1,16 @@ +@group(0) @binding(0) var output: array; + +@compute @workgroup_size(64, 1, 1) +fn main( + @builtin(global_invocation_id) + global_id : vec3, + + @builtin(local_invocation_id) + local_id : vec3, +) { + if (global_id.x >= arrayLength(&output)) { + return; + } + output[global_id.x] = + f32(global_id.x) * 1000. + f32(local_id.x); +} diff --git a/src/core/examples/map-async/main.zig b/src/core/examples/map-async/main.zig new file mode 100644 index 00000000..fc323a35 --- /dev/null +++ b/src/core/examples/map-async/main.zig @@ -0,0 +1,101 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; + +pub const App = @This(); + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +const workgroup_size = 64; +const buffer_size = 1000; + +pub fn init(app: *App) !void { + try core.init(.{}); + app.* = .{}; + + const output = core.device.createBuffer(&.{ + .usage = .{ .storage = true, .copy_src = true }, + .size = buffer_size * @sizeOf(f32), + .mapped_at_creation = .false, + }); + defer output.release(); + + const staging = core.device.createBuffer(&.{ + .usage = .{ .map_read = true, .copy_dst = true }, + .size = buffer_size * @sizeOf(f32), + .mapped_at_creation = .false, + }); + defer staging.release(); + + const compute_module = core.device.createShaderModuleWGSL("main.wgsl", @embedFile("main.wgsl")); + + const compute_pipeline = core.device.createComputePipeline(&gpu.ComputePipeline.Descriptor{ .compute = gpu.ProgrammableStageDescriptor{ + .module = compute_module, + .entry_point = "main", + } }); + defer compute_pipeline.release(); + + const layout = compute_pipeline.getBindGroupLayout(0); + defer layout.release(); + + const compute_bind_group = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{ + .layout = layout, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, output, 0, buffer_size * @sizeOf(f32)), + }, + })); + defer compute_bind_group.release(); + + compute_module.release(); + + const encoder = core.device.createCommandEncoder(null); + + const compute_pass = encoder.beginComputePass(null); + compute_pass.setPipeline(compute_pipeline); + compute_pass.setBindGroup(0, compute_bind_group, &.{}); + compute_pass.dispatchWorkgroups(try std.math.divCeil(u32, buffer_size, workgroup_size), 1, 1); + compute_pass.end(); + compute_pass.release(); + + encoder.copyBufferToBuffer(output, 0, staging, 0, buffer_size * @sizeOf(f32)); + + var command = encoder.finish(null); + encoder.release(); + + var response: gpu.Buffer.MapAsyncStatus = undefined; + const callback = (struct { + pub inline fn callback(ctx: *gpu.Buffer.MapAsyncStatus, status: gpu.Buffer.MapAsyncStatus) void { + ctx.* = status; + } + }).callback; + + var queue = core.queue; + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + + staging.mapAsync(.{ .read = true }, 0, buffer_size * @sizeOf(f32), &response, callback); + while (true) { + if (response == gpu.Buffer.MapAsyncStatus.success) { + break; + } else { + core.device.tick(); + } + } + + const staging_mapped = staging.getConstMappedRange(f32, 0, buffer_size); + for (staging_mapped.?) |v| { + std.debug.print("{d} ", .{v}); + } + std.debug.print("\n", .{}); + staging.unmap(); +} + +pub fn deinit(app: *App) void { + _ = app; + defer _ = gpa.deinit(); + core.deinit(); +} + +pub fn update(_: *App) !bool { + return true; +} diff --git a/src/core/examples/pbr-basic/main.zig b/src/core/examples/pbr-basic/main.zig new file mode 100644 index 00000000..b5ca6e16 --- /dev/null +++ b/src/core/examples/pbr-basic/main.zig @@ -0,0 +1,920 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const m3d = @import("model3d"); +const zm = @import("zmath"); +const assets = @import("assets"); +const VertexWriter = @import("vertex_writer.zig").VertexWriter; + +pub const App = @This(); + +const Vec4 = [4]f32; +const Vec3 = [3]f32; +const Vec2 = [2]f32; +const Mat4 = [4]Vec4; + +fn Dimensions2D(comptime T: type) type { + return struct { + width: T, + height: T, + }; +} + +const Vertex = extern struct { + position: Vec3, + normal: Vec3, +}; + +const Model = struct { + vertex_count: u32, + index_count: u32, + vertex_buffer: *gpu.Buffer, + index_buffer: *gpu.Buffer, +}; + +const Material = struct { + const Params = extern struct { + roughness: f32, + metallic: f32, + color: Vec3, + }; + + name: []const u8, + params: Params, +}; + +const PressedKeys = packed struct(u16) { + right: bool = false, + left: bool = false, + up: bool = false, + down: bool = false, + padding: u12 = undefined, + + pub inline fn areKeysPressed(self: @This()) bool { + return (self.up or self.down or self.left or self.right); + } + + pub inline fn clear(self: *@This()) void { + self.right = false; + self.left = false; + self.up = false; + self.down = false; + } +}; + +const Camera = struct { + const Matrices = struct { + perspective: Mat4 = [1]Vec4{[1]f32{0.0} ** 4} ** 4, + view: Mat4 = [1]Vec4{[1]f32{0.0} ** 4} ** 4, + }; + + rotation: Vec3 = .{ 0.0, 0.0, 0.0 }, + position: Vec3 = .{ 0.0, 0.0, 0.0 }, + view_position: Vec4 = .{ 0.0, 0.0, 0.0, 0.0 }, + fov: f32 = 0.0, + znear: f32 = 0.0, + zfar: f32 = 0.0, + rotation_speed: f32 = 0.0, + movement_speed: f32 = 0.0, + updated: bool = false, + matrices: Matrices = .{}, + + pub fn calculateMovement(self: *@This(), pressed_keys: PressedKeys) void { + std.debug.assert(pressed_keys.areKeysPressed()); + const rotation_radians = Vec3{ + toRadians(self.rotation[0]), + toRadians(self.rotation[1]), + toRadians(self.rotation[2]), + }; + var camera_front = zm.Vec{ -zm.cos(rotation_radians[0]) * zm.sin(rotation_radians[1]), zm.sin(rotation_radians[0]), zm.cos(rotation_radians[0]) * zm.cos(rotation_radians[1]), 0 }; + camera_front = zm.normalize3(camera_front); + if (pressed_keys.up) { + camera_front[0] *= self.movement_speed; + camera_front[1] *= self.movement_speed; + camera_front[2] *= self.movement_speed; + self.position = Vec3{ + self.position[0] + camera_front[0], + self.position[1] + camera_front[1], + self.position[2] + camera_front[2], + }; + } + if (pressed_keys.down) { + camera_front[0] *= self.movement_speed; + camera_front[1] *= self.movement_speed; + camera_front[2] *= self.movement_speed; + self.position = Vec3{ + self.position[0] - camera_front[0], + self.position[1] - camera_front[1], + self.position[2] - camera_front[2], + }; + } + if (pressed_keys.right) { + camera_front = zm.cross3(.{ 0.0, 1.0, 0.0, 0.0 }, camera_front); + camera_front = zm.normalize3(camera_front); + camera_front[0] *= self.movement_speed; + camera_front[1] *= self.movement_speed; + camera_front[2] *= self.movement_speed; + self.position = Vec3{ + self.position[0] - camera_front[0], + self.position[1] - camera_front[1], + self.position[2] - camera_front[2], + }; + } + if (pressed_keys.left) { + camera_front = zm.cross3(.{ 0.0, 1.0, 0.0, 0.0 }, camera_front); + camera_front = zm.normalize3(camera_front); + camera_front[0] *= self.movement_speed; + camera_front[1] *= self.movement_speed; + camera_front[2] *= self.movement_speed; + self.position = Vec3{ + self.position[0] + camera_front[0], + self.position[1] + camera_front[1], + self.position[2] + camera_front[2], + }; + } + self.updateViewMatrix(); + } + + fn updateViewMatrix(self: *@This()) void { + const rotation_x = zm.rotationX(toRadians(self.rotation[2])); + const rotation_y = zm.rotationY(toRadians(self.rotation[1])); + const rotation_z = zm.rotationZ(toRadians(self.rotation[0])); + const rotation_matrix = zm.mul(rotation_z, zm.mul(rotation_x, rotation_y)); + + const translation_matrix: zm.Mat = zm.translationV(.{ + self.position[0], + self.position[1], + self.position[2], + 0, + }); + const view = zm.mul(translation_matrix, rotation_matrix); + self.matrices.view[0] = view[0]; + self.matrices.view[1] = view[1]; + self.matrices.view[2] = view[2]; + self.matrices.view[3] = view[3]; + self.view_position = .{ + -self.position[0], + self.position[1], + -self.position[2], + 0.0, + }; + self.updated = true; + } + + pub fn setMovementSpeed(self: *@This(), speed: f32) void { + self.movement_speed = speed; + } + + pub fn setPerspective(self: *@This(), fov: f32, aspect: f32, znear: f32, zfar: f32) void { + self.fov = fov; + self.znear = znear; + self.zfar = zfar; + const perspective = zm.perspectiveFovRhGl(toRadians(fov), aspect, znear, zfar); + self.matrices.perspective[0] = perspective[0]; + self.matrices.perspective[1] = perspective[1]; + self.matrices.perspective[2] = perspective[2]; + self.matrices.perspective[3] = perspective[3]; + } + + pub fn setRotationSpeed(self: *@This(), speed: f32) void { + self.rotation_speed = speed; + } + + pub fn setRotation(self: *@This(), rotation: Vec3) void { + self.rotation = rotation; + self.updateViewMatrix(); + } + + pub fn rotate(self: *@This(), delta: Vec2) void { + self.rotation[0] -= delta[1]; + self.rotation[1] -= delta[0]; + self.updateViewMatrix(); + } + + pub fn setPosition(self: *@This(), position: Vec3) void { + self.position = .{ + position[0], + -position[1], + position[2], + }; + self.updateViewMatrix(); + } +}; + +const UniformBuffers = struct { + const Params = struct { + buffer: *gpu.Buffer, + buffer_size: u64, + model_size: u64, + }; + const Buffer = struct { + buffer: *gpu.Buffer, + size: u32, + }; + ubo_matrices: Buffer, + ubo_params: Buffer, + material_params: Params, + object_params: Params, +}; + +const UboParams = struct { + lights: [4]Vec4, +}; + +const UboMatrices = extern struct { + projection: Mat4, + model: Mat4, + view: Mat4, + camera_position: Vec3, +}; + +const grid_element_count = grid_dimensions * grid_dimensions; + +const MaterialParamsDynamic = extern struct { + roughness: f32 = 0, + metallic: f32 = 0, + color: Vec3 = .{ 0, 0, 0 }, + padding: [236]u8 = [1]u8{0} ** 236, +}; +const MaterialParamsDynamicGrid = [grid_element_count]MaterialParamsDynamic; + +const ObjectParamsDynamic = extern struct { + position: Vec3 = .{ 0, 0, 0 }, + padding: [244]u8 = [1]u8{0} ** 244, +}; +const ObjectParamsDynamicGrid = [grid_element_count]ObjectParamsDynamic; + +// +// Globals +// + +const material_names = [11][:0]const u8{ + "Gold", "Copper", "Chromium", "Nickel", "Titanium", "Cobalt", "Platinum", + // Testing materials + "White", "Red", "Blue", "Black", +}; + +const object_names = [5][:0]const u8{ "Sphere", "Teapot", "Torusknot", "Venus", "Stanford Dragon" }; + +const materials = [_]Material{ + .{ .name = "Gold", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 1.0, 0.765557, 0.336057 } } }, + .{ .name = "Copper", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.955008, 0.637427, 0.538163 } } }, + .{ .name = "Chromium", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.549585, 0.556114, 0.554256 } } }, + .{ .name = "Nickel", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 1.0, 0.608679, 0.525649 } } }, + .{ .name = "Titanium", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.541931, 0.496791, 0.449419 } } }, + .{ .name = "Cobalt", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.662124, 0.654864, 0.633732 } } }, + .{ .name = "Platinum", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.672411, 0.637331, 0.585456 } } }, + // Testing colors + .{ .name = "White", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 1.0, 1.0, 1.0 } } }, + .{ .name = "Red", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 1.0, 0.0, 0.0 } } }, + .{ .name = "Blue", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.0, 0.0, 1.0 } } }, + .{ .name = "Black", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.0, 0.0, 0.0 } } }, +}; + +const grid_dimensions = 7; +const model_embeds = [_][:0]const u8{ + assets.sphere_m3d, + assets.teapot_m3d, + assets.torusknot_m3d, + assets.venus_m3d, + assets.stanford_dragon_m3d, +}; + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +// +// Member variables +// + +title_timer: core.Timer, +timer: core.Timer, +camera: Camera, +render_pipeline: *gpu.RenderPipeline, +render_pass_descriptor: gpu.RenderPassDescriptor, +bind_group: *gpu.BindGroup, +color_attachment: gpu.RenderPassColorAttachment, +depth_stencil_attachment_description: gpu.RenderPassDepthStencilAttachment, +depth_texture: *gpu.Texture, +depth_texture_view: *gpu.TextureView, +pressed_keys: PressedKeys, +models: [5]Model, +ubo_params: UboParams, +ubo_matrices: UboMatrices, +uniform_buffers: UniformBuffers, +material_params_dynamic: MaterialParamsDynamicGrid = [1]MaterialParamsDynamic{.{}} ** grid_element_count, +object_params_dynamic: ObjectParamsDynamicGrid = [1]ObjectParamsDynamic{.{}} ** grid_element_count, +uniform_buffers_dirty: bool, +buffers_bound: bool, +is_paused: bool, +current_material_index: usize, +current_object_index: usize, +mouse_position: core.Position, +is_rotating: bool, + +// +// Functions +// + +pub fn init(app: *App) !void { + try core.init(.{}); + app.timer = try core.Timer.start(); + app.title_timer = try core.Timer.start(); + + app.pressed_keys = .{}; + app.buffers_bound = false; + app.is_paused = false; + app.uniform_buffers_dirty = false; + app.current_material_index = 0; + app.current_object_index = 0; + app.mouse_position = .{ .x = 0, .y = 0 }; + app.is_rotating = false; + + setupCamera(app); + try loadModels(std.heap.c_allocator, app); + prepareUniformBuffers(app); + setupPipeline(app); + setupRenderPass(app); + app.printControls(); +} + +pub fn deinit(app: *App) void { + defer _ = gpa.deinit(); + defer core.deinit(); + + app.bind_group.release(); + app.render_pipeline.release(); + app.depth_texture_view.release(); + app.depth_texture.release(); + app.uniform_buffers.ubo_matrices.buffer.release(); + app.uniform_buffers.ubo_params.buffer.release(); + app.uniform_buffers.material_params.buffer.release(); + app.uniform_buffers.object_params.buffer.release(); +} + +pub fn update(app: *App) !bool { + var iter = core.pollEvents(); + while (iter.next()) |event| { + app.updateUI(event); + switch (event) { + .mouse_motion => |ev| { + if (app.is_rotating) { + const delta = Vec2{ + @as(f32, @floatCast((app.mouse_position.x - ev.pos.x) * app.camera.rotation_speed)), + @as(f32, @floatCast((app.mouse_position.y - ev.pos.y) * app.camera.rotation_speed)), + }; + app.mouse_position = ev.pos; + app.camera.rotate(delta); + app.uniform_buffers_dirty = true; + } + }, + .mouse_press => |ev| { + if (ev.button == .left) { + app.is_rotating = true; + app.mouse_position = ev.pos; + } + }, + .mouse_release => |ev| { + if (ev.button == .left) { + app.is_rotating = false; + } + }, + .key_press, .key_repeat => |ev| { + const key = ev.key; + if (key == .up or key == .w) app.pressed_keys.up = true; + if (key == .down or key == .s) app.pressed_keys.down = true; + if (key == .left or key == .a) app.pressed_keys.left = true; + if (key == .right or key == .d) app.pressed_keys.right = true; + }, + .framebuffer_resize => |ev| { + app.depth_texture_view.release(); + app.depth_texture.release(); + app.depth_texture = core.device.createTexture(&gpu.Texture.Descriptor{ + .usage = .{ .render_attachment = true }, + .format = .depth24_plus_stencil8, + .sample_count = 1, + .size = .{ + .width = ev.width, + .height = ev.height, + .depth_or_array_layers = 1, + }, + }); + app.depth_texture_view = app.depth_texture.createView(&gpu.TextureView.Descriptor{ + .format = .depth24_plus_stencil8, + .dimension = .dimension_2d, + .array_layer_count = 1, + .aspect = .all, + }); + app.depth_stencil_attachment_description = gpu.RenderPassDepthStencilAttachment{ + .view = app.depth_texture_view, + .depth_load_op = .clear, + .depth_store_op = .store, + .depth_clear_value = 1.0, + .stencil_clear_value = 0, + .stencil_load_op = .clear, + .stencil_store_op = .store, + }; + + const aspect_ratio = @as(f32, @floatFromInt(ev.width)) / @as(f32, @floatFromInt(ev.height)); + app.camera.setPerspective(60.0, aspect_ratio, 0.1, 256.0); + app.uniform_buffers_dirty = true; + }, + .close => return true, + else => {}, + } + } + if (app.pressed_keys.areKeysPressed()) { + app.camera.calculateMovement(app.pressed_keys); + app.pressed_keys.clear(); + app.uniform_buffers_dirty = true; + } + + if (app.uniform_buffers_dirty) { + updateUniformBuffers(app); + app.uniform_buffers_dirty = false; + } + + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + app.color_attachment.view = back_buffer_view; + app.render_pass_descriptor = gpu.RenderPassDescriptor{ + .color_attachment_count = 1, + .color_attachments = &[_]gpu.RenderPassColorAttachment{app.color_attachment}, + .depth_stencil_attachment = &app.depth_stencil_attachment_description, + }; + const encoder = core.device.createCommandEncoder(null); + const current_model = app.models[app.current_object_index]; + + const pass = encoder.beginRenderPass(&app.render_pass_descriptor); + + const dimensions = Dimensions2D(f32){ + .width = @as(f32, @floatFromInt(core.descriptor.width)), + .height = @as(f32, @floatFromInt(core.descriptor.height)), + }; + pass.setViewport( + 0, + 0, + dimensions.width, + dimensions.height, + 0.0, + 1.0, + ); + pass.setScissorRect(0, 0, core.descriptor.width, core.descriptor.height); + pass.setPipeline(app.render_pipeline); + + if (!app.is_paused) { + app.updateLights(); + } + + var i: usize = 0; + while (i < (grid_dimensions * grid_dimensions)) : (i += 1) { + const alignment = 256; + const dynamic_offset: u32 = @as(u32, @intCast(i)) * alignment; + const dynamic_offsets = [2]u32{ dynamic_offset, dynamic_offset }; + pass.setBindGroup(0, app.bind_group, &dynamic_offsets); + if (!app.buffers_bound) { + pass.setVertexBuffer(0, current_model.vertex_buffer, 0, @sizeOf(Vertex) * current_model.vertex_count); + pass.setIndexBuffer(current_model.index_buffer, .uint32, 0, gpu.whole_size); + app.buffers_bound = true; + } + pass.drawIndexed( + current_model.index_count, // index_count + 1, // instance_count + 0, // first_index + 0, // base_vertex + 0, // first_instance + ); + } + + pass.end(); + pass.release(); + + var command = encoder.finish(null); + encoder.release(); + + const queue = core.queue; + queue.submit(&[_]*gpu.CommandBuffer{command}); + + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); + app.buffers_bound = false; + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("PBR Basic [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} + +fn prepareUniformBuffers(app: *App) void { + comptime { + std.debug.assert(@sizeOf(ObjectParamsDynamic) == 256); + std.debug.assert(@sizeOf(MaterialParamsDynamic) == 256); + } + + app.uniform_buffers.ubo_matrices.size = roundToMultipleOf4(u32, @as(u32, @intCast(@sizeOf(UboMatrices)))) + 4; + app.uniform_buffers.ubo_matrices.buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = app.uniform_buffers.ubo_matrices.size, + .mapped_at_creation = .false, + }); + + app.uniform_buffers.ubo_params.size = roundToMultipleOf4(u32, @as(u32, @intCast(@sizeOf(UboParams)))) + 4; + app.uniform_buffers.ubo_params.buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = app.uniform_buffers.ubo_params.size, + .mapped_at_creation = .false, + }); + + // + // Material parameter uniform buffer + // + app.uniform_buffers.material_params.model_size = @sizeOf(Vec2) + @sizeOf(Vec3); + app.uniform_buffers.material_params.buffer_size = calculateConstantBufferByteSize(@sizeOf(MaterialParamsDynamicGrid)); + std.debug.assert(app.uniform_buffers.material_params.buffer_size >= app.uniform_buffers.material_params.model_size); + app.uniform_buffers.material_params.buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = app.uniform_buffers.material_params.buffer_size, + .mapped_at_creation = .false, + }); + + // + // Object parameter uniform buffer + // + app.uniform_buffers.object_params.model_size = @sizeOf(Vec3) + 4; + app.uniform_buffers.object_params.buffer_size = calculateConstantBufferByteSize(@sizeOf(MaterialParamsDynamicGrid)) + 4; + std.debug.assert(app.uniform_buffers.object_params.buffer_size >= app.uniform_buffers.object_params.model_size); + app.uniform_buffers.object_params.buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = app.uniform_buffers.object_params.buffer_size, + .mapped_at_creation = .false, + }); + + app.updateUniformBuffers(); + app.updateDynamicUniformBuffer(); + app.updateLights(); +} + +fn updateDynamicUniformBuffer(app: *App) void { + var index: u32 = 0; + var y: usize = 0; + while (y < grid_dimensions) : (y += 1) { + var x: usize = 0; + while (x < grid_dimensions) : (x += 1) { + const grid_dimensions_float = @as(f32, @floatFromInt(grid_dimensions)); + app.object_params_dynamic[index].position[0] = (@as(f32, @floatFromInt(x)) - (grid_dimensions_float / 2) * 2.5); + app.object_params_dynamic[index].position[1] = 0; + app.object_params_dynamic[index].position[2] = (@as(f32, @floatFromInt(y)) - (grid_dimensions_float / 2) * 2.5); + app.material_params_dynamic[index].metallic = zm.clamp(@as(f32, @floatFromInt(x)) / (grid_dimensions_float - 1), 0.1, 1.0); + app.material_params_dynamic[index].roughness = zm.clamp(@as(f32, @floatFromInt(y)) / (grid_dimensions_float - 1), 0.05, 1.0); + app.material_params_dynamic[index].color = materials[app.current_material_index].params.color; + index += 1; + } + } + const queue = core.queue; + queue.writeBuffer( + app.uniform_buffers.object_params.buffer, + 0, + &app.object_params_dynamic, + ); + queue.writeBuffer( + app.uniform_buffers.material_params.buffer, + 0, + &app.material_params_dynamic, + ); +} + +fn updateUniformBuffers(app: *App) void { + app.ubo_matrices.projection = app.camera.matrices.perspective; + app.ubo_matrices.view = app.camera.matrices.view; + const rotation_degrees = if (app.current_object_index == 1) @as(f32, -45.0) else @as(f32, -90.0); + const model = zm.rotationY(rotation_degrees); + app.ubo_matrices.model[0] = model[0]; + app.ubo_matrices.model[1] = model[1]; + app.ubo_matrices.model[2] = model[2]; + app.ubo_matrices.model[3] = model[3]; + app.ubo_matrices.camera_position = .{ + -app.camera.position[0], + -app.camera.position[1], + -app.camera.position[2], + }; + const queue = core.queue; + queue.writeBuffer(app.uniform_buffers.ubo_matrices.buffer, 0, &[_]UboMatrices{app.ubo_matrices}); +} + +fn updateLights(app: *App) void { + const p: f32 = 15.0; + app.ubo_params.lights[0] = Vec4{ -p, -p * 0.5, -p, 1.0 }; + app.ubo_params.lights[1] = Vec4{ -p, -p * 0.5, p, 1.0 }; + app.ubo_params.lights[2] = Vec4{ p, -p * 0.5, p, 1.0 }; + app.ubo_params.lights[3] = Vec4{ p, -p * 0.5, -p, 1.0 }; + const base_value = toRadians(@mod(app.timer.read() * 0.1, 1.0) * 360.0); + app.ubo_params.lights[0][0] = @sin(base_value) * 20.0; + app.ubo_params.lights[0][2] = @cos(base_value) * 20.0; + app.ubo_params.lights[1][0] = @cos(base_value) * 20.0; + app.ubo_params.lights[1][1] = @sin(base_value) * 20.0; + const queue = core.queue; + queue.writeBuffer( + app.uniform_buffers.ubo_params.buffer, + 0, + &[_]UboParams{app.ubo_params}, + ); +} + +fn setupPipeline(app: *App) void { + comptime { + std.debug.assert(@sizeOf(Vertex) == @sizeOf(f32) * 6); + } + + const bind_group_layout_entries = [_]gpu.BindGroupLayout.Entry{ + .{ + .binding = 0, + .visibility = .{ .vertex = true, .fragment = true }, + .buffer = .{ + .type = .uniform, + .has_dynamic_offset = .false, + .min_binding_size = app.uniform_buffers.ubo_matrices.size, + }, + }, + .{ + .binding = 1, + .visibility = .{ .fragment = true }, + .buffer = .{ + .type = .uniform, + .has_dynamic_offset = .false, + .min_binding_size = app.uniform_buffers.ubo_params.size, + }, + }, + .{ + .binding = 2, + .visibility = .{ .fragment = true }, + .buffer = .{ + .type = .uniform, + .has_dynamic_offset = .true, + .min_binding_size = app.uniform_buffers.material_params.model_size, + }, + }, + .{ + .binding = 3, + .visibility = .{ .vertex = true }, + .buffer = .{ + .type = .uniform, + .has_dynamic_offset = .true, + .min_binding_size = app.uniform_buffers.object_params.model_size, + }, + }, + }; + + const bind_group_layout = core.device.createBindGroupLayout( + &gpu.BindGroupLayout.Descriptor.init(.{ + .entries = bind_group_layout_entries[0..], + }), + ); + + const bind_group_layouts = [_]*gpu.BindGroupLayout{bind_group_layout}; + const pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &bind_group_layouts, + })); + + const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{ + .array_stride = @sizeOf(Vertex), + .step_mode = .vertex, + .attributes = &.{ + .{ .format = .float32x3, .offset = @offsetOf(Vertex, "position"), .shader_location = 0 }, + .{ .format = .float32x3, .offset = @offsetOf(Vertex, "normal"), .shader_location = 1 }, + }, + }); + + const blend_component_descriptor = gpu.BlendComponent{ + .operation = .add, + .src_factor = .one, + .dst_factor = .zero, + }; + + const color_target_state = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &.{ + .color = blend_component_descriptor, + .alpha = blend_component_descriptor, + }, + }; + + const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .layout = pipeline_layout, + .primitive = .{ + .cull_mode = .back, + }, + .depth_stencil = &.{ + .format = .depth24_plus_stencil8, + .depth_write_enabled = .true, + .depth_compare = .less, + }, + .fragment = &gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "frag_main", + .targets = &.{color_target_state}, + }), + .vertex = gpu.VertexState.init(.{ + .module = shader_module, + .entry_point = "vertex_main", + .buffers = &.{vertex_buffer_layout}, + }), + }; + app.render_pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + shader_module.release(); + + { + const bind_group_entries = [_]gpu.BindGroup.Entry{ + .{ + .binding = 0, + .buffer = app.uniform_buffers.ubo_matrices.buffer, + .size = app.uniform_buffers.ubo_matrices.size, + }, + .{ + .binding = 1, + .buffer = app.uniform_buffers.ubo_params.buffer, + .size = app.uniform_buffers.ubo_params.size, + }, + .{ + .binding = 2, + .buffer = app.uniform_buffers.material_params.buffer, + .size = app.uniform_buffers.material_params.model_size, + }, + .{ + .binding = 3, + .buffer = app.uniform_buffers.object_params.buffer, + .size = app.uniform_buffers.object_params.model_size, + }, + }; + app.bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bind_group_layout, + .entries = &bind_group_entries, + }), + ); + } +} + +fn setupRenderPass(app: *App) void { + app.color_attachment = gpu.RenderPassColorAttachment{ + .clear_value = .{ + .r = 0.0, + .g = 0.0, + .b = 0.0, + .a = 0.0, + }, + .load_op = .clear, + .store_op = .store, + }; + + app.depth_texture = core.device.createTexture(&.{ + .usage = .{ .render_attachment = true, .copy_src = true }, + .format = .depth24_plus_stencil8, + .sample_count = 1, + .size = .{ + .width = core.descriptor.width, + .height = core.descriptor.height, + .depth_or_array_layers = 1, + }, + }); + + app.depth_texture_view = app.depth_texture.createView(&.{ + .format = .depth24_plus_stencil8, + .dimension = .dimension_2d, + .array_layer_count = 1, + .aspect = .all, + }); + + app.depth_stencil_attachment_description = gpu.RenderPassDepthStencilAttachment{ + .view = app.depth_texture_view, + .depth_load_op = .clear, + .depth_store_op = .store, + .depth_clear_value = 1.0, + .stencil_clear_value = 0, + .stencil_load_op = .clear, + .stencil_store_op = .store, + }; +} + +fn loadModels(allocator: std.mem.Allocator, app: *App) !void { + for (model_embeds, 0..) |model_data, model_data_i| { + const m3d_model = m3d.load(model_data, null, null, null) orelse return error.LoadModelFailed; + + const vertex_count = m3d_model.handle.numvertex; + const face_count = m3d_model.handle.numface; + + var model: *Model = &app.models[model_data_i]; + + model.index_count = face_count * 3; + + var vertex_writer = try VertexWriter(Vertex, u32).init(allocator, face_count * 3, vertex_count, face_count * 3); + defer vertex_writer.deinit(allocator); + + const scale: f32 = 0.45; + const vertices = m3d_model.handle.vertex[0..vertex_count]; + var i: usize = 0; + while (i < face_count) : (i += 1) { + const face = m3d_model.handle.face[i]; + var x: usize = 0; + while (x < 3) : (x += 1) { + const vertex_index = face.vertex[x]; + const normal_index = face.normal[x]; + const vertex = Vertex{ + .position = .{ + vertices[vertex_index].x * scale, + vertices[vertex_index].y * scale, + vertices[vertex_index].z * scale, + }, + .normal = .{ + vertices[normal_index].x, + vertices[normal_index].y, + vertices[normal_index].z, + }, + }; + vertex_writer.put(vertex, vertex_index); + } + } + + const vertex_buffer = vertex_writer.vertexBuffer(); + const index_buffer = vertex_writer.indexBuffer(); + + model.vertex_count = @as(u32, @intCast(vertex_buffer.len)); + + model.vertex_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .vertex = true }, + .size = @sizeOf(Vertex) * model.vertex_count, + .mapped_at_creation = .false, + }); + const queue = core.queue; + queue.writeBuffer(model.vertex_buffer, 0, vertex_buffer); + + model.index_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .index = true }, + .size = @sizeOf(u32) * model.index_count, + .mapped_at_creation = .false, + }); + queue.writeBuffer(model.index_buffer, 0, index_buffer); + } +} + +fn printControls(app: *App) void { + std.debug.print("[controls]\n", .{}); + std.debug.print("[p] paused: {}\n", .{app.is_paused}); + std.debug.print("[m] material: {s}\n", .{material_names[app.current_material_index]}); + std.debug.print("[o] object: {s}\n", .{object_names[app.current_object_index]}); +} + +fn updateUI(app: *App, event: core.Event) void { + switch (event) { + .key_press => |ev| { + var update_uniform_buffers: bool = false; + switch (ev.key) { + .p => app.is_paused = !app.is_paused, + .m => { + app.current_material_index = (app.current_material_index + 1) % material_names.len; + update_uniform_buffers = true; + }, + .o => { + app.current_object_index = (app.current_object_index + 1) % object_names.len; + update_uniform_buffers = true; + }, + else => return, + } + app.printControls(); + if (update_uniform_buffers) { + updateDynamicUniformBuffer(app); + } + }, + else => {}, + } +} + +fn setupCamera(app: *App) void { + app.camera = Camera{ + .rotation_speed = 1.0, + .movement_speed = 1.0, + }; + const aspect_ratio: f32 = @as(f32, @floatFromInt(core.descriptor.width)) / @as(f32, @floatFromInt(core.descriptor.height)); + app.camera.setPosition(.{ 10.0, 6.0, 6.0 }); + app.camera.setRotation(.{ 62.5, 90.0, 0.0 }); + app.camera.setMovementSpeed(0.5); + app.camera.setPerspective(60.0, aspect_ratio, 0.1, 256.0); + app.camera.setRotationSpeed(0.25); +} + +inline fn roundToMultipleOf4(comptime T: type, value: T) T { + return (value + 3) & ~@as(T, 3); +} + +inline fn calculateConstantBufferByteSize(byte_size: usize) usize { + return (byte_size + 255) & ~@as(usize, 255); +} + +inline fn toRadians(degrees: f32) f32 { + return degrees * (std.math.pi / 180.0); +} diff --git a/src/core/examples/pbr-basic/shader.wgsl b/src/core/examples/pbr-basic/shader.wgsl new file mode 100644 index 00000000..adf728cc --- /dev/null +++ b/src/core/examples/pbr-basic/shader.wgsl @@ -0,0 +1,118 @@ +@group(0) @binding(0) var ubo : UBO; +@group(0) @binding(1) var uboParams : UBOShared; +@group(0) @binding(2) var material : MaterialParams; +@group(0) @binding(3) var object : ObjectParams; + +struct VertexOut { + @builtin(position) position_clip : vec4, + @location(0) fragPosition : vec3, + @location(1) fragNormal : vec3, +} + +struct MaterialParams { + roughness : f32, + metallic : f32, + r : f32, + g : f32, + b : f32 +} + +struct UBOShared { + lights : array, 4>, +} + +struct UBO { + projection : mat4x4, + model : mat4x4, + view : mat4x4, + camPos : vec3, +} + +struct ObjectParams { + position : vec3 +} + +@vertex fn vertex_main( + @location(0) position : vec3, + @location(1) normal : vec3 +) -> VertexOut { + var output : VertexOut; + var locPos = vec4(ubo.model * vec4(position, 1.0)); + output.fragPosition = locPos.xyz + object.position; + output.fragNormal = mat3x3(ubo.model[0].xyz, ubo.model[1].xyz, ubo.model[2].xyz) * normal; + output.position_clip = ubo.projection * ubo.view * vec4(output.fragPosition, 1.0); + return output; +} + +@fragment fn frag_main( + @location(0) position : vec3, + @location(1) normal: vec3 +) -> @location(0) vec4 { + var N : vec3 = normalize(normal); + var V : vec3 = normalize(ubo.camPos - position); + var Lo = vec3(0.0); + // Specular contribution + for(var i: i32 = 0; i < 4; i++) { + var L : vec3 = normalize(uboParams.lights[i].xyz - position); + Lo += BRDF(L, V, N, material.metallic, material.roughness); + } + // Combine with ambient + var color : vec3 = material_color() * 0.02; + color += Lo; + // Gamma correct + color = pow(color, vec3(0.4545)); + return vec4(color, 1.0); +} + +const PI : f32 = 3.14159265359; + +fn material_color() -> vec3 { + return vec3(material.r, material.g, material.b); +} + +// Normal Distribution function -------------------------------------- +fn D_GGX(dotNH : f32, roughness : f32) -> f32 { + var alpha : f32 = roughness * roughness; + var alpha2 : f32 = alpha * alpha; + var denom : f32 = dotNH * dotNH * (alpha2 - 1.0) + 1.0; + return alpha2 / (PI * denom * denom); +} + +// Geometric Shadowing function -------------------------------------- +fn G_SchlicksmithGGX(dotNL : f32, dotNV : f32, roughness : f32) -> f32 { + var r : f32 = roughness + 1.0; + var k : f32 = (r * r) / 8.0; + var GL : f32 = dotNL / (dotNL * (1.0 - k) + k); + var GV : f32 = dotNV / (dotNV * (1.0 - k) + k); + return GL * GV; +} + +// Fresnel function ---------------------------------------------------- +fn F_Schlick(cosTheta : f32, metallic : f32) -> vec3 { + var F0 : vec3 = mix(vec3(0.04), material_color(), metallic); + var F : vec3 = F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); + return F; +} + +// Specular BRDF composition -------------------------------------------- +fn BRDF(L : vec3, V : vec3, N : vec3, metallic : f32, roughness : f32) -> vec3 { + var H : vec3 = normalize(V + L); + var dotNV : f32 = clamp(dot(N, V), 0.0, 1.0); + var dotNL : f32 = clamp(dot(N, L), 0.0, 1.0); + var dotLH : f32 = clamp(dot(L, H), 0.0, 1.0); + var dotNH : f32 = clamp(dot(N, H), 0.0, 1.0); + var lightColor = vec3(1.0); + var color = vec3(0.0); + if(dotNL > 0.0) { + var rroughness : f32 = max(0.05, roughness); + // D = Normal distribution (Distribution of the microfacets) + var D : f32 = D_GGX(dotNH, roughness); + // G = Geometric shadowing term (Microfacets shadowing) + var G : f32 = G_SchlicksmithGGX(dotNL, dotNV, roughness); + // F = Fresnel factor (Reflectance depending on angle of incidence) + var F : vec3 = F_Schlick(dotNV, metallic); + var spec : vec3 = (D * F * G) / (4.0 * dotNL * dotNV); + color += spec * dotNL * lightColor; + } + return color; +} \ No newline at end of file diff --git a/src/core/examples/pbr-basic/vertex_writer.zig b/src/core/examples/pbr-basic/vertex_writer.zig new file mode 100644 index 00000000..1610981e --- /dev/null +++ b/src/core/examples/pbr-basic/vertex_writer.zig @@ -0,0 +1,188 @@ +const std = @import("std"); + +/// Vertex writer manages the placement of vertices by tracking which are unique. If a duplicate vertex is added +/// with `put`, only it's index will be written to the index buffer. +/// `IndexType` should match the integer type used for the index buffer +pub fn VertexWriter(comptime VertexType: type, comptime IndexType: type) type { + return struct { + const MapEntry = struct { + packed_index: IndexType = null_index, + next_sparse: IndexType = null_index, + }; + + const null_index: IndexType = std.math.maxInt(IndexType); + + vertices: []VertexType, + indices: []IndexType, + sparse_to_packed_map: []MapEntry, + + /// Next index outside of the 1:1 mapping range for storing + /// position -> normal collisions + next_collision_index: IndexType, + + /// Next packed index + next_packed_index: IndexType, + written_indices_count: IndexType, + + /// Allocate storage and set default values + /// `sparse_vertices_count` is the number of vertices in the source before de-duplication / remapping + /// Put more succinctly, the largest index value in source index buffer + /// `max_vertex_count` is largest permutation of vertices assuming that {vertex, uv, normal} never map 1:1 and always + /// create a new mapping + pub fn init( + allocator: std.mem.Allocator, + indices_count: IndexType, + sparse_vertices_count: IndexType, + max_vertex_count: IndexType, + ) !@This() { + var result: @This() = undefined; + result.vertices = try allocator.alloc(VertexType, max_vertex_count); + result.indices = try allocator.alloc(IndexType, indices_count); + result.sparse_to_packed_map = try allocator.alloc(MapEntry, max_vertex_count); + result.next_collision_index = sparse_vertices_count; + result.next_packed_index = 0; + result.written_indices_count = 0; + @memset(result.sparse_to_packed_map, .{}); + return result; + } + + pub fn put(self: *@This(), vertex: VertexType, sparse_index: IndexType) void { + if (self.sparse_to_packed_map[sparse_index].packed_index == null_index) { + // New start of chain, reserve a new packed index and add entry to `index_map` + const packed_index = self.next_packed_index; + self.sparse_to_packed_map[sparse_index].packed_index = packed_index; + self.vertices[packed_index] = vertex; + self.indices[self.written_indices_count] = packed_index; + self.written_indices_count += 1; + self.next_packed_index += 1; + return; + } + var previous_sparse_index: IndexType = undefined; + var current_sparse_index = sparse_index; + while (current_sparse_index != null_index) { + const packed_index = self.sparse_to_packed_map[current_sparse_index].packed_index; + if (std.mem.eql(u8, &std.mem.toBytes(self.vertices[packed_index]), &std.mem.toBytes(vertex))) { + // We already have a record for this vertex in our chain + self.indices[self.written_indices_count] = packed_index; + self.written_indices_count += 1; + return; + } + previous_sparse_index = current_sparse_index; + current_sparse_index = self.sparse_to_packed_map[current_sparse_index].next_sparse; + } + // This is a new mapping for the given sparse index + const packed_index = self.next_packed_index; + const remapped_sparse_index = self.next_collision_index; + self.indices[self.written_indices_count] = packed_index; + self.vertices[packed_index] = vertex; + self.sparse_to_packed_map[previous_sparse_index].next_sparse = remapped_sparse_index; + self.sparse_to_packed_map[remapped_sparse_index].packed_index = packed_index; + self.next_packed_index += 1; + self.next_collision_index += 1; + self.written_indices_count += 1; + } + + pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void { + allocator.free(self.vertices); + allocator.free(self.indices); + allocator.free(self.sparse_to_packed_map); + } + + pub fn indexBuffer(self: @This()) []IndexType { + return self.indices; + } + + pub fn vertexBuffer(self: @This()) []VertexType { + return self.vertices[0..self.next_packed_index]; + } + }; +} + +test "VertexWriter" { + const Vec3 = [3]f32; + const Vertex = extern struct { + position: Vec3, + normal: Vec3, + }; + + const expect = std.testing.expect; + const allocator = std.testing.allocator; + + const Face = struct { + position: [3]u16, + normal: [3]u16, + }; + + const vertices = [_]Vec3{ + Vec3{ 1.0, 0.0, 0.0 }, // 0: Position + Vec3{ 2.0, 0.0, 0.0 }, // 1: Position + Vec3{ 3.0, 0.0, 0.0 }, // 2: Position + Vec3{ 1.0, 0.0, 0.0 }, // 3: Normal + Vec3{ 4.0, 0.0, 0.0 }, // 4: Position + Vec3{ 0.0, 1.0, 0.0 }, // 5: Normal + Vec3{ 5.0, 0.0, 0.0 }, // 6: Position + Vec3{ 0.0, 0.0, 1.0 }, // 7: Normal + Vec3{ 1.0, 0.0, 1.0 }, // 8: Normal + Vec3{ 6.0, 0.0, 0.0 }, // 9: Position + }; + + const faces = [_]Face{ + .{ .position = .{ 0, 4, 2 }, .normal = .{ 7, 5, 3 } }, + .{ .position = .{ 2, 3, 9 }, .normal = .{ 3, 7, 8 } }, + .{ .position = .{ 9, 2, 4 }, .normal = .{ 8, 7, 5 } }, + .{ .position = .{ 2, 6, 1 }, .normal = .{ 3, 5, 7 } }, + .{ .position = .{ 9, 6, 0 }, .normal = .{ 5, 7, 8 } }, + }; + + var writer = try VertexWriter(Vertex, u32).init( + allocator, + faces.len * 3, // indices count + vertices.len, // original vertices count + faces.len * 3, // maximum vertices count + ); + defer writer.deinit(allocator); + + for (faces) |face| { + var x: usize = 0; + while (x < 3) : (x += 1) { + const position_index = face.position[x]; + const position = vertices[position_index]; + const normal = vertices[face.normal[x]]; + const vertex = Vertex{ + .position = position, + .normal = normal, + }; + writer.put(vertex, position_index); + } + } + + const indices = writer.indexBuffer(); + try expect(indices.len == faces.len * 3); + + // Face 0 + try expect(indices[0] == 0); // (0, 7) New + try expect(indices[1] == 1); // (4, 5) New + try expect(indices[2] == 2); // (2, 3) New + + // Face 1 + try expect(indices[3 + 0] == 2); // (2, 3) Duplicate - Reuse index + try expect(indices[3 + 1] == 3); // (3, 7) New + try expect(indices[3 + 2] == 4); // (9, 8) New + + // Face 2 + try expect(indices[6 + 0] == 4); // (9, 8) Duplicate - Reuse index + try expect(indices[6 + 1] == 5); // (2, 7) New normal mapping (Don't clobber) + try expect(indices[6 + 2] == 1); // (4, 5) Duplicate - Reuse Index + + // Face 3 + try expect(indices[9 + 0] == 2); // (2, 3) Duplicate - Reuse index + try expect(indices[9 + 1] == 6); // (6, 5) New + try expect(indices[9 + 2] == 7); // (1, 7) New + + // Face 4 + try expect(indices[12 + 0] == 8); // (9, 5) New normal mapping (Don't clobber) + try expect(indices[12 + 1] == 9); // (6, 7) New normal mapping (Don't clobber) + try expect(indices[12 + 2] == 10); // (0, 8) New normal mapping (Don't clobber) + + try expect(writer.vertexBuffer().len == 11); +} diff --git a/src/core/examples/pixel-post-process/cube_mesh.zig b/src/core/examples/pixel-post-process/cube_mesh.zig new file mode 100644 index 00000000..edf25840 --- /dev/null +++ b/src/core/examples/pixel-post-process/cube_mesh.zig @@ -0,0 +1,49 @@ +pub const Vertex = extern struct { + pos: @Vector(3, f32), + normal: @Vector(3, f32), + uv: @Vector(2, f32), +}; + +pub const vertices = [_]Vertex{ + .{ .pos = .{ 1, -1, 1 }, .normal = .{ 0, -1, 0 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, 1 }, .normal = .{ 0, -1, 0 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, -1, -1 }, .normal = .{ 0, -1, 0 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, -1, -1 }, .normal = .{ 0, -1, 0 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, -1, 1 }, .normal = .{ 0, -1, 0 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, -1 }, .normal = .{ 0, -1, 0 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ 1, 1, 1 }, .normal = .{ 1, 0, 0 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, -1, 1 }, .normal = .{ 1, 0, 0 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, -1, -1 }, .normal = .{ 1, 0, 0 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, 1, -1 }, .normal = .{ 1, 0, 0 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, 1, 1 }, .normal = .{ 1, 0, 0 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, -1, -1 }, .normal = .{ 1, 0, 0 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ -1, 1, 1 }, .normal = .{ 0, 1, 0 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, 1, 1 }, .normal = .{ 0, 1, 0 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, 1, -1 }, .normal = .{ 0, 1, 0 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, 1, -1 }, .normal = .{ 0, 1, 0 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, 1, 1 }, .normal = .{ 0, 1, 0 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, 1, -1 }, .normal = .{ 0, 1, 0 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ -1, -1, 1 }, .normal = .{ -1, 0, 0 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, 1 }, .normal = .{ -1, 0, 0 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, -1 }, .normal = .{ -1, 0, 0 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, -1, -1 }, .normal = .{ -1, 0, 0 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, -1, 1 }, .normal = .{ -1, 0, 0 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, -1 }, .normal = .{ -1, 0, 0 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ 1, 1, 1 }, .normal = .{ 0, 0, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, 1 }, .normal = .{ 0, 0, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, -1, 1 }, .normal = .{ 0, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, -1, 1 }, .normal = .{ 0, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, -1, 1 }, .normal = .{ 0, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, 1, 1 }, .normal = .{ 0, 0, 1 }, .uv = .{ 1, 1 } }, + + .{ .pos = .{ 1, -1, -1 }, .normal = .{ 0, 0, -1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, -1 }, .normal = .{ 0, 0, -1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, -1 }, .normal = .{ 0, 0, -1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, 1, -1 }, .normal = .{ 0, 0, -1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, -1, -1 }, .normal = .{ 0, 0, -1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, -1 }, .normal = .{ 0, 0, -1 }, .uv = .{ 0, 0 } }, +}; diff --git a/src/core/examples/pixel-post-process/main.zig b/src/core/examples/pixel-post-process/main.zig new file mode 100644 index 00000000..b39b7410 --- /dev/null +++ b/src/core/examples/pixel-post-process/main.zig @@ -0,0 +1,461 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const zm = @import("zmath"); + +const Vertex = @import("cube_mesh.zig").Vertex; +const vertices = @import("cube_mesh.zig").vertices; +const Quad = @import("quad_mesh.zig").Quad; +const quad = @import("quad_mesh.zig").quad; + +pub const App = @This(); + +const pixel_size = 8; + +const UniformBufferObject = struct { + mat: zm.Mat, +}; +const PostUniformBufferObject = extern struct { + width: u32, + height: u32, + pixel_size: u32 = pixel_size, +}; +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +title_timer: core.Timer, +timer: core.Timer, + +pipeline: *gpu.RenderPipeline, +normal_pipeline: *gpu.RenderPipeline, +vertex_buffer: *gpu.Buffer, +uniform_buffer: *gpu.Buffer, +bind_group: *gpu.BindGroup, + +post_pipeline: *gpu.RenderPipeline, +post_vertex_buffer: *gpu.Buffer, +post_uniform_buffer: *gpu.Buffer, +post_bind_group: *gpu.BindGroup, + +draw_texture_view: *gpu.TextureView, +depth_texture_view: *gpu.TextureView, +normal_texture_view: *gpu.TextureView, + +pub fn init(app: *App) !void { + try core.init(.{}); + app.title_timer = try core.Timer.start(); + app.timer = try core.Timer.start(); + + try app.createRenderTextures(); + app.createDrawPipeline(); + app.createPostPipeline(); +} + +pub fn deinit(app: *App) void { + defer _ = gpa.deinit(); + defer core.deinit(); + + app.cleanup(); +} + +pub fn update(app: *App) !bool { + var iter = core.pollEvents(); + while (iter.next()) |event| { + switch (event) { + .key_press => |ev| { + if (ev.key == .space) return true; + }, + .close => return true, + .framebuffer_resize => { + app.cleanup(); + try app.createRenderTextures(); + app.createDrawPipeline(); + app.createPostPipeline(); + }, + else => {}, + } + } + + const size = core.size(); + const encoder = core.device.createCommandEncoder(null); + encoder.writeBuffer(app.post_uniform_buffer, 0, &[_]PostUniformBufferObject{ + PostUniformBufferObject{ + .width = size.width, + .height = size.height, + }, + }); + + { + const time = app.timer.read() * 0.5; + const model = zm.mul(zm.rotationX(time * (std.math.pi / 2.0)), zm.rotationZ(time * (std.math.pi / 2.0))); + const view = zm.lookAtRh( + zm.Vec{ 0, 5, 2, 1 }, + zm.Vec{ 0, 0, 0, 1 }, + zm.Vec{ 0, 0, 1, 0 }, + ); + const proj = zm.perspectiveFovRh( + (std.math.pi / 4.0), + @as(f32, @floatFromInt(core.descriptor.width)) / @as(f32, @floatFromInt(core.descriptor.height)), + 0.1, + 10, + ); + const mvp = zm.mul(zm.mul(model, view), proj); + const ubo = UniformBufferObject{ + .mat = zm.transpose(mvp), + }; + encoder.writeBuffer(app.uniform_buffer, 0, &[_]UniformBufferObject{ubo}); + } + + { + // render scene to downscaled texture + const color_attachment = gpu.RenderPassColorAttachment{ + .view = app.draw_texture_view, + .clear_value = std.mem.zeroes(gpu.Color), + .load_op = .clear, + .store_op = .store, + }; + const render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + .depth_stencil_attachment = &gpu.RenderPassDepthStencilAttachment{ + .view = app.depth_texture_view, + .depth_load_op = .clear, + .depth_store_op = .store, + .depth_clear_value = 1.0, + }, + }); + + const pass = encoder.beginRenderPass(&render_pass_info); + pass.setPipeline(app.pipeline); + pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len); + pass.setBindGroup(0, app.bind_group, &.{0}); + pass.draw(vertices.len, 1, 0, 0); + pass.end(); + pass.release(); + } + + { + // render scene normals to texture + const normal_color_attachment = gpu.RenderPassColorAttachment{ + .view = app.normal_texture_view, + .clear_value = .{ .r = 0.5, .b = 0.5, .g = 0.5, .a = 1.0 }, + .load_op = .clear, + .store_op = .store, + }; + const normal_render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{normal_color_attachment}, + }); + + const normal_pass = encoder.beginRenderPass(&normal_render_pass_info); + normal_pass.setPipeline(app.normal_pipeline); + normal_pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len); + normal_pass.setBindGroup(0, app.bind_group, &.{0}); + normal_pass.draw(vertices.len, 1, 0, 0); + normal_pass.end(); + normal_pass.release(); + } + + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + { + // render to swap chain using previous passes + const post_color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = std.mem.zeroes(gpu.Color), + .load_op = .clear, + .store_op = .store, + }; + const post_render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{post_color_attachment}, + }); + + const draw_pass = encoder.beginRenderPass(&post_render_pass_info); + draw_pass.setPipeline(app.post_pipeline); + draw_pass.setVertexBuffer(0, app.post_vertex_buffer, 0, @sizeOf(Quad) * quad.len); + draw_pass.setBindGroup(0, app.post_bind_group, &.{0}); + draw_pass.draw(quad.len, 1, 0, 0); + draw_pass.end(); + draw_pass.release(); + } + + var command = encoder.finish(null); + encoder.release(); + + const queue = core.queue; + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Pixel Post Process [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} + +fn cleanup(app: *App) void { + app.pipeline.release(); + app.normal_pipeline.release(); + app.vertex_buffer.release(); + app.uniform_buffer.release(); + app.bind_group.release(); + + app.post_pipeline.release(); + app.post_vertex_buffer.release(); + app.post_uniform_buffer.release(); + app.post_bind_group.release(); + + app.draw_texture_view.release(); + app.depth_texture_view.release(); + app.normal_texture_view.release(); +} + +fn createRenderTextures(app: *App) !void { + const size = core.size(); + + const draw_texture_desc = gpu.Texture.Descriptor.init(.{ + .size = .{ .width = size.width / pixel_size, .height = size.height / pixel_size }, + .format = .bgra8_unorm, + .usage = .{ .texture_binding = true, .copy_dst = true, .render_attachment = true }, + }); + const draw_texture = core.device.createTexture(&draw_texture_desc); + app.draw_texture_view = draw_texture.createView(null); + draw_texture.release(); + + const depth_texture_desc = gpu.Texture.Descriptor.init(.{ + .size = .{ .width = size.width / pixel_size, .height = size.height / pixel_size }, + .format = .depth32_float, + .usage = .{ .texture_binding = true, .copy_dst = true, .render_attachment = true }, + }); + const depth_texture = core.device.createTexture(&depth_texture_desc); + app.depth_texture_view = depth_texture.createView(null); + depth_texture.release(); + + const normal_texture_desc = gpu.Texture.Descriptor.init(.{ + .size = .{ .width = size.width / pixel_size, .height = size.height / pixel_size }, + .format = .bgra8_unorm, + .usage = .{ .texture_binding = true, .copy_dst = true, .render_attachment = true }, + }); + const normal_texture = core.device.createTexture(&normal_texture_desc); + app.normal_texture_view = normal_texture.createView(null); + normal_texture.release(); +} + +fn createDrawPipeline(app: *App) void { + const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); + const vertex_attributes = [_]gpu.VertexAttribute{ + .{ .format = .float32x3, .offset = @offsetOf(Vertex, "pos"), .shader_location = 0 }, + .{ .format = .float32x3, .offset = @offsetOf(Vertex, "normal"), .shader_location = 1 }, + .{ .format = .float32x2, .offset = @offsetOf(Vertex, "uv"), .shader_location = 2 }, + }; + const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{ + .array_stride = @sizeOf(Vertex), + .step_mode = .vertex, + .attributes = &vertex_attributes, + }); + const vertex = gpu.VertexState.init(.{ + .module = shader_module, + .entry_point = "vertex_main", + .buffers = &.{vertex_buffer_layout}, + }); + + const blend = gpu.BlendState{}; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + + const bgle = gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, true, 0); + const bgl = core.device.createBindGroupLayout( + &gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &.{bgle}, + }), + ); + + const bind_group_layouts = [_]*gpu.BindGroupLayout{bgl}; + const pipeline_layout = core.device.createPipelineLayout( + &gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &bind_group_layouts, + }), + ); + + const vertex_buffer = core.device.createBuffer(&.{ + .usage = .{ .vertex = true }, + .size = @sizeOf(Vertex) * vertices.len, + .mapped_at_creation = .true, + }); + const vertex_mapped = vertex_buffer.getMappedRange(Vertex, 0, vertices.len); + @memcpy(vertex_mapped.?, vertices[0..]); + vertex_buffer.unmap(); + + const uniform_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = @sizeOf(UniformBufferObject), + .mapped_at_creation = .false, + }); + const bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bgl, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, uniform_buffer, 0, @sizeOf(UniformBufferObject)), + }, + }), + ); + + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + .layout = pipeline_layout, + .vertex = vertex, + .primitive = .{ + .cull_mode = .back, + }, + .depth_stencil = &gpu.DepthStencilState{ + .format = .depth32_float, + .depth_write_enabled = .true, + .depth_compare = .less, + }, + }; + + { + // "same" pipeline, different fragment shader to create a texture with normal information + const normal_fs_module = core.device.createShaderModuleWGSL("normal_frag.wgsl", @embedFile("normal_frag.wgsl")); + const normal_fragment = gpu.FragmentState.init(.{ + .module = normal_fs_module, + .entry_point = "main", + .targets = &.{color_target}, + }); + const normal_pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &normal_fragment, + .layout = pipeline_layout, + .vertex = vertex, + .primitive = .{ + .cull_mode = .back, + }, + }; + app.normal_pipeline = core.device.createRenderPipeline(&normal_pipeline_descriptor); + + normal_fs_module.release(); + } + + app.pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + app.vertex_buffer = vertex_buffer; + app.uniform_buffer = uniform_buffer; + app.bind_group = bind_group; + + shader_module.release(); + pipeline_layout.release(); + bgl.release(); +} + +fn createPostPipeline(app: *App) void { + const vs_module = core.device.createShaderModuleWGSL("pixel_vert.wgsl", @embedFile("pixel_vert.wgsl")); + const vertex_attributes = [_]gpu.VertexAttribute{ + .{ .format = .float32x3, .offset = @offsetOf(Quad, "pos"), .shader_location = 0 }, + .{ .format = .float32x2, .offset = @offsetOf(Quad, "uv"), .shader_location = 1 }, + }; + const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{ + .array_stride = @sizeOf(Quad), + .step_mode = .vertex, + .attributes = &vertex_attributes, + }); + const vertex = gpu.VertexState.init(.{ + .module = vs_module, + .entry_point = "main", + .buffers = &.{vertex_buffer_layout}, + }); + + const fs_module = core.device.createShaderModuleWGSL("pixel_frag.wgsl", @embedFile("pixel_frag.wgsl")); + const blend = gpu.BlendState{}; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = gpu.FragmentState.init(.{ + .module = fs_module, + .entry_point = "main", + .targets = &.{color_target}, + }); + + const bgl = core.device.createBindGroupLayout( + &gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &.{ + gpu.BindGroupLayout.Entry.texture(0, .{ .fragment = true }, .float, .dimension_2d, false), + gpu.BindGroupLayout.Entry.sampler(1, .{ .fragment = true }, .filtering), + gpu.BindGroupLayout.Entry.texture(2, .{ .fragment = true }, .depth, .dimension_2d, false), + gpu.BindGroupLayout.Entry.sampler(3, .{ .fragment = true }, .filtering), + gpu.BindGroupLayout.Entry.texture(4, .{ .fragment = true }, .float, .dimension_2d, false), + gpu.BindGroupLayout.Entry.sampler(5, .{ .fragment = true }, .filtering), + gpu.BindGroupLayout.Entry.buffer(6, .{ .fragment = true }, .uniform, true, 0), + }, + }), + ); + + const bind_group_layouts = [_]*gpu.BindGroupLayout{bgl}; + const pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &bind_group_layouts, + })); + + const vertex_buffer = core.device.createBuffer(&.{ + .usage = .{ .vertex = true }, + .size = @sizeOf(Quad) * quad.len, + .mapped_at_creation = .true, + }); + const vertex_mapped = vertex_buffer.getMappedRange(Quad, 0, quad.len); + @memcpy(vertex_mapped.?, quad[0..]); + vertex_buffer.unmap(); + + const draw_sampler = core.device.createSampler(null); + const depth_sampler = core.device.createSampler(null); + const normal_sampler = core.device.createSampler(null); + const uniform_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = @sizeOf(PostUniformBufferObject), + .mapped_at_creation = .false, + }); + const bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bgl, + .entries = &[_]gpu.BindGroup.Entry{ + gpu.BindGroup.Entry.textureView(0, app.draw_texture_view), + gpu.BindGroup.Entry.sampler(1, draw_sampler), + gpu.BindGroup.Entry.textureView(2, app.depth_texture_view), + gpu.BindGroup.Entry.sampler(3, depth_sampler), + gpu.BindGroup.Entry.textureView(4, app.normal_texture_view), + gpu.BindGroup.Entry.sampler(5, normal_sampler), + gpu.BindGroup.Entry.buffer(6, uniform_buffer, 0, @sizeOf(PostUniformBufferObject)), + }, + }), + ); + draw_sampler.release(); + depth_sampler.release(); + normal_sampler.release(); + + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + .layout = pipeline_layout, + .vertex = vertex, + .primitive = .{ + .cull_mode = .back, + }, + }; + + app.post_pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + app.post_vertex_buffer = vertex_buffer; + app.post_uniform_buffer = uniform_buffer; + app.post_bind_group = bind_group; + + vs_module.release(); + fs_module.release(); + pipeline_layout.release(); + bgl.release(); +} diff --git a/src/core/examples/pixel-post-process/normal_frag.wgsl b/src/core/examples/pixel-post-process/normal_frag.wgsl new file mode 100644 index 00000000..28f7407c --- /dev/null +++ b/src/core/examples/pixel-post-process/normal_frag.wgsl @@ -0,0 +1,6 @@ +@fragment fn main( + @location(0) normal: vec3, + @location(1) uv: vec2, +) -> @location(0) vec4 { + return vec4(normal / 2 + 0.5, 1.0); +} diff --git a/src/core/examples/pixel-post-process/pixel_frag.wgsl b/src/core/examples/pixel-post-process/pixel_frag.wgsl new file mode 100644 index 00000000..6932530f --- /dev/null +++ b/src/core/examples/pixel-post-process/pixel_frag.wgsl @@ -0,0 +1,74 @@ +@group(0) @binding(0) +var draw_texture: texture_2d; +@group(0) @binding(1) +var draw_texture_sampler: sampler; + +@group(0) @binding(2) +var depth_texture: texture_depth_2d; +@group(0) @binding(3) +var depth_texture_sampler: sampler; + +@group(0) @binding(4) +var normal_texture: texture_2d; +@group(0) @binding(5) +var normal_texture_sampler: sampler; + +struct View { + @location(0) width: u32, + @location(1) height: u32, + @location(2) pixel_size: u32, +} +@group(0) @binding(6) +var view: View; + +fn sample_depth(uv: vec2, x: f32, y: f32) -> f32 { + return textureSample( + depth_texture, + depth_texture_sampler, + uv + vec2(x * f32(view.pixel_size) / f32(view.width), y * f32(view.pixel_size) / f32(view.height)) + ); +} + +fn sample_normal(uv: vec2, x: f32, y: f32) -> vec3 { + return textureSample( + normal_texture, + normal_texture_sampler, + uv + vec2(x * f32(view.pixel_size) / f32(view.width), y * f32(view.pixel_size) / f32(view.height)) + ).xyz; +} + +fn normal_indicator(uv: vec2, x: f32, y: f32) -> f32 { + var depth_diff = sample_depth(uv, 0, 0) - sample_depth(uv, x, y); + var dx = sample_normal(uv, 0, 0); + var dy = sample_normal(uv, x, y); + if (depth_diff > 0) { + // only sample normals from closest pixel + return 0; + } + return distance(dx, dy); +} + +@fragment fn main( + @builtin(position) position: vec4, + @location(0) uv: vec2 +) -> @location(0) vec4 { + var depth = sample_depth(uv, 0, 0); + var depth_diff: f32 = 0; + depth_diff += abs(depth - sample_depth(uv, -1, 0)); + depth_diff += abs(depth - sample_depth(uv, 1, 0)); + depth_diff += abs(depth - sample_depth(uv, 0, -1)); + depth_diff += abs(depth - sample_depth(uv, 0, 1)); + + var normal_diff: f32 = 0; + normal_diff += normal_indicator(uv, -1, 0); + normal_diff += normal_indicator(uv, 1, 0); + normal_diff += normal_indicator(uv, 0, -1); + normal_diff += normal_indicator(uv, 0, 1); + + var color = textureSample(draw_texture, draw_texture_sampler, uv); + if (depth_diff > 0.007) { // magic number from testing + return color * 0.7; + } + // add instead of multiply so really dark pixels get brighter + return color + (vec4(1) * step(0.1, normal_diff) * 0.7); +} diff --git a/src/core/examples/pixel-post-process/pixel_vert.wgsl b/src/core/examples/pixel-post-process/pixel_vert.wgsl new file mode 100644 index 00000000..734f629e --- /dev/null +++ b/src/core/examples/pixel-post-process/pixel_vert.wgsl @@ -0,0 +1,14 @@ +struct VertexOut { + @builtin(position) position_clip: vec4, + @location(0) uv: vec2 +} + +@vertex fn main( + @location(0) position: vec3, + @location(1) uv: vec2 +) -> VertexOut { + var output : VertexOut; + output.position_clip = vec4(position.xy, 0.0, 1.0); + output.uv = uv; + return output; +} diff --git a/src/core/examples/pixel-post-process/quad_mesh.zig b/src/core/examples/pixel-post-process/quad_mesh.zig new file mode 100644 index 00000000..57db212f --- /dev/null +++ b/src/core/examples/pixel-post-process/quad_mesh.zig @@ -0,0 +1,13 @@ +pub const Quad = extern struct { + pos: @Vector(3, f32), + uv: @Vector(2, f32), +}; + +pub const quad = [_]Quad{ + .{ .pos = .{ -1.0, 1.0, 0.0 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1.0, -1.0, 0.0 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1.0, 1.0, 0.0 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1.0, 1.0, 0.0 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1.0, -1.0, 0.0 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1.0, -1.0, 0.0 }, .uv = .{ 1, 0 } }, +}; diff --git a/src/core/examples/pixel-post-process/shader.wgsl b/src/core/examples/pixel-post-process/shader.wgsl new file mode 100644 index 00000000..325391cd --- /dev/null +++ b/src/core/examples/pixel-post-process/shader.wgsl @@ -0,0 +1,27 @@ +@group(0) @binding(0) var ubo: mat4x4; + +struct VertexOut { + @builtin(position) position_clip: vec4, + @location(0) normal: vec3, + @location(1) uv: vec2, +} + +@vertex fn vertex_main( + @location(0) position: vec3, + @location(1) normal: vec3, + @location(2) uv: vec2 +) -> VertexOut { + var output: VertexOut; + output.position_clip = vec4(position, 1) * ubo; + output.normal = (vec4(normal, 0) * ubo).xyz; + output.uv = uv; + return output; +} + +@fragment fn frag_main( + @location(0) normal: vec3, + @location(1) uv: vec2, +) -> @location(0) vec4 { + var color = floor((uv * 0.5 + 0.25) * 32) / 32; + return vec4(color, 1, 1); +} \ No newline at end of file diff --git a/src/core/examples/procedural-primitives/main.zig b/src/core/examples/procedural-primitives/main.zig new file mode 100644 index 00000000..c0b24e31 --- /dev/null +++ b/src/core/examples/procedural-primitives/main.zig @@ -0,0 +1,62 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const renderer = @import("renderer.zig"); + +pub const App = @This(); + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +title_timer: core.Timer, + +pub fn init(app: *App) !void { + try core.init(.{ .required_limits = gpu.Limits{ + .max_vertex_buffers = 1, + .max_vertex_attributes = 2, + .max_bind_groups = 1, + .max_uniform_buffers_per_shader_stage = 1, + .max_uniform_buffer_binding_size = 16 * 1 * @sizeOf(f32), + } }); + + const allocator = gpa.allocator(); + const timer = try core.Timer.start(); + try renderer.init(allocator, timer); + app.* = .{ .title_timer = try core.Timer.start() }; +} + +pub fn deinit(app: *App) void { + _ = app; + defer _ = gpa.deinit(); + defer core.deinit(); + defer renderer.deinit(); +} + +pub fn update(app: *App) !bool { + var iter = core.pollEvents(); + while (iter.next()) |event| { + switch (event) { + .key_press => |ev| { + if (ev.key == .space) return true; + if (ev.key == .right) { + renderer.curr_primitive_index += 1; + renderer.curr_primitive_index %= 7; + } + }, + .close => return true, + else => {}, + } + } + + renderer.update(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Procedural Primitives [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} diff --git a/src/core/examples/procedural-primitives/procedural-primitives.zig b/src/core/examples/procedural-primitives/procedural-primitives.zig new file mode 100644 index 00000000..ae1c74ab --- /dev/null +++ b/src/core/examples/procedural-primitives/procedural-primitives.zig @@ -0,0 +1,336 @@ +const std = @import("std"); +const zmath = @import("zmath"); + +const PI = 3.1415927410125732421875; + +pub const F32x3 = @Vector(3, f32); +pub const F32x4 = @Vector(4, f32); +pub const VertexData = struct { + position: F32x3, + normal: F32x3, +}; + +pub const PrimitiveType = enum(u4) { none, triangle, quad, plane, circle, uv_sphere, ico_sphere, cylinder, cone, torus }; + +pub const Primitive = struct { + vertex_data: std.ArrayList(VertexData), + vertex_count: u32, + index_data: std.ArrayList(u32), + index_count: u32, + type: PrimitiveType = .none, +}; + +// 2D Primitives +pub fn createTrianglePrimitive(allocator: std.mem.Allocator, size: f32) !Primitive { + const vertex_count = 3; + const index_count = 3; + var vertex_data = try std.ArrayList(VertexData).initCapacity(allocator, vertex_count); + + const edge = size / 2.0; + + vertex_data.appendSliceAssumeCapacity(&[vertex_count]VertexData{ + VertexData{ .position = F32x3{ -edge, -edge, 0.0 }, .normal = F32x3{ -edge, -edge, 0.0 } }, + VertexData{ .position = F32x3{ edge, -edge, 0.0 }, .normal = F32x3{ edge, -edge, 0.0 } }, + VertexData{ .position = F32x3{ 0.0, edge, 0.0 }, .normal = F32x3{ 0.0, edge, 0.0 } }, + }); + + var index_data = try std.ArrayList(u32).initCapacity(allocator, index_count); + index_data.appendSliceAssumeCapacity(&[index_count]u32{ 0, 1, 2 }); + + return Primitive{ .vertex_data = vertex_data, .vertex_count = 3, .index_data = index_data, .index_count = 3, .type = .triangle }; +} + +pub fn createQuadPrimitive(allocator: std.mem.Allocator, size: f32) !Primitive { + const vertex_count = 4; + const index_count = 6; + var vertex_data = try std.ArrayList(VertexData).initCapacity(allocator, vertex_count); + + const edge = size / 2.0; + + vertex_data.appendSliceAssumeCapacity(&[vertex_count]VertexData{ + VertexData{ .position = F32x3{ -edge, -edge, 0.0 }, .normal = F32x3{ -edge, -edge, 0.0 } }, + VertexData{ .position = F32x3{ edge, -edge, 0.0 }, .normal = F32x3{ edge, -edge, 0.0 } }, + VertexData{ .position = F32x3{ -edge, edge, 0.0 }, .normal = F32x3{ -edge, edge, 0.0 } }, + VertexData{ .position = F32x3{ edge, edge, 0.0 }, .normal = F32x3{ edge, edge, 0.0 } }, + }); + + var index_data = try std.ArrayList(u32).initCapacity(allocator, index_count); + index_data.appendSliceAssumeCapacity(&[index_count]u32{ + 0, 1, 2, + 1, 3, 2, + }); + + return Primitive{ .vertex_data = vertex_data, .vertex_count = 4, .index_data = index_data, .index_count = 6, .type = .quad }; +} + +pub fn createPlanePrimitive(allocator: std.mem.Allocator, x_subdivision: u32, y_subdivision: u32, size: f32) !Primitive { + const x_num_vertices = x_subdivision + 1; + const y_num_vertices = y_subdivision + 1; + const vertex_count = x_num_vertices * y_num_vertices; + var vertex_data = try std.ArrayList(VertexData).initCapacity(allocator, vertex_count); + + const vertices_distance_y = (size / @as(f32, @floatFromInt(y_subdivision))); + const vertices_distance_x = (size / @as(f32, @floatFromInt(x_subdivision))); + var y: u32 = 0; + while (y < y_num_vertices) : (y += 1) { + var x: u32 = 0; + const pos_y = (-size / 2.0) + @as(f32, @floatFromInt(y)) * vertices_distance_y; + while (x < x_num_vertices) : (x += 1) { + const pos_x = (-size / 2.0) + @as(f32, @floatFromInt(x)) * vertices_distance_x; + vertex_data.appendAssumeCapacity(VertexData{ .position = F32x3{ pos_x, pos_y, 0.0 }, .normal = F32x3{ pos_x, pos_y, 0.0 } }); + } + } + + const index_count = x_subdivision * y_subdivision * 2 * 3; + var index_data = try std.ArrayList(u32).initCapacity(allocator, index_count); + + y = 0; + while (y < y_subdivision) : (y += 1) { + var x: u32 = 0; + while (x < x_subdivision) : (x += 1) { + // First Triangle of Quad + index_data.appendAssumeCapacity(x + y * y_num_vertices); + index_data.appendAssumeCapacity(x + 1 + y * y_num_vertices); + index_data.appendAssumeCapacity(x + (y + 1) * y_num_vertices); + + // Second Triangle of Quad + index_data.appendAssumeCapacity(x + 1 + y * y_num_vertices); + index_data.appendAssumeCapacity(x + (y + 1) * y_num_vertices + 1); + index_data.appendAssumeCapacity(x + (y + 1) * y_num_vertices); + } + } + + return Primitive{ .vertex_data = vertex_data, .vertex_count = vertex_count, .index_data = index_data, .index_count = index_count, .type = .plane }; +} + +pub fn createCirclePrimitive(allocator: std.mem.Allocator, vertices: u32, radius: f32) !Primitive { + const vertex_count = vertices + 1; + var vertex_data = try std.ArrayList(VertexData).initCapacity(allocator, vertex_count); + + // Mid point of circle + vertex_data.appendAssumeCapacity(VertexData{ .position = F32x3{ 0, 0, 0.0 }, .normal = F32x3{ 0, 0, 0.0 } }); + + var x: u32 = 0; + const angle = 2 * PI / @as(f32, @floatFromInt(vertices)); + while (x < vertices) : (x += 1) { + const x_f = @as(f32, @floatFromInt(x)); + const pos_x = radius * zmath.cos(angle * x_f); + const pos_y = radius * zmath.sin(angle * x_f); + + vertex_data.appendAssumeCapacity(VertexData{ .position = F32x3{ pos_x, pos_y, 0.0 }, .normal = F32x3{ pos_x, pos_y, 0.0 } }); + } + + const index_count = (vertices + 1) * 3; + var index_data = try std.ArrayList(u32).initCapacity(allocator, index_count); + + x = 1; + while (x <= vertices) : (x += 1) { + index_data.appendAssumeCapacity(0); + index_data.appendAssumeCapacity(x); + index_data.appendAssumeCapacity(x + 1); + } + + index_data.appendAssumeCapacity(0); + index_data.appendAssumeCapacity(vertices); + index_data.appendAssumeCapacity(1); + + return Primitive{ .vertex_data = vertex_data, .vertex_count = vertex_count, .index_data = index_data, .index_count = index_count, .type = .plane }; +} + +// 3D Primitives +pub fn createCubePrimitive(allocator: std.mem.Allocator, size: f32) !Primitive { + const vertex_count = 8; + const index_count = 36; + var vertex_data = try std.ArrayList(VertexData).initCapacity(allocator, vertex_count); + + const edge = size / 2.0; + + vertex_data.appendSliceAssumeCapacity(&[vertex_count]VertexData{ + // Front positions + VertexData{ .position = F32x3{ -edge, -edge, edge }, .normal = F32x3{ -edge, -edge, edge } }, + VertexData{ .position = F32x3{ edge, -edge, edge }, .normal = F32x3{ edge, -edge, edge } }, + VertexData{ .position = F32x3{ edge, edge, edge }, .normal = F32x3{ edge, edge, edge } }, + VertexData{ .position = F32x3{ -edge, edge, edge }, .normal = F32x3{ -edge, edge, edge } }, + // Back positions + VertexData{ .position = F32x3{ -edge, -edge, -edge }, .normal = F32x3{ -edge, -edge, -edge } }, + VertexData{ .position = F32x3{ edge, -edge, -edge }, .normal = F32x3{ edge, -edge, -edge } }, + VertexData{ .position = F32x3{ edge, edge, -edge }, .normal = F32x3{ edge, edge, -edge } }, + VertexData{ .position = F32x3{ -edge, edge, -edge }, .normal = F32x3{ -edge, edge, -edge } }, + }); + + var index_data = try std.ArrayList(u32).initCapacity(allocator, index_count); + + index_data.appendSliceAssumeCapacity(&[index_count]u32{ + // front quad + 0, 1, 2, + 2, 3, 0, + // right quad + 1, 5, 6, + 6, 2, 1, + // back quad + 7, 6, 5, + 5, 4, 7, + // left quad + 4, 0, 3, + 3, 7, 4, + // bottom quad + 4, 5, 1, + 1, 0, 4, + // top quad + 3, 2, 6, + 6, 7, 3, + }); + + return Primitive{ .vertex_data = vertex_data, .vertex_count = vertex_count, .index_data = index_data, .index_count = index_count, .type = .quad }; +} + +const VertexDataMAL = std.MultiArrayList(VertexData); + +pub fn createCylinderPrimitive(allocator: std.mem.Allocator, radius: f32, height: f32, num_sides: u32) !Primitive { + const alloc_amt_vert: u32 = num_sides * 2 + 2; + const alloc_amt_idx: u32 = num_sides * 12; + + var vertex_data = VertexDataMAL{}; + try vertex_data.ensureTotalCapacity(allocator, alloc_amt_vert); + defer vertex_data.deinit(allocator); + + var out_vertex_data = try std.ArrayList(VertexData).initCapacity(allocator, alloc_amt_vert); + var index_data = try std.ArrayList(u32).initCapacity(allocator, alloc_amt_idx); + + vertex_data.appendAssumeCapacity(VertexData{ .position = F32x3{ 0.0, (height / 2.0), 0.0 }, .normal = undefined }); + vertex_data.appendAssumeCapacity(VertexData{ .position = F32x3{ 0.0, -(height / 2.0), 0.0 }, .normal = undefined }); + + const angle = 2.0 * PI / @as(f32, @floatFromInt(num_sides)); + + for (1..num_sides + 1) |i| { + const float_i = @as(f32, @floatFromInt(i)); + + const x: f32 = radius * zmath.sin(angle * float_i); + const y: f32 = radius * zmath.cos(angle * float_i); + + vertex_data.appendAssumeCapacity(VertexData{ .position = F32x3{ x, (height / 2.0), y }, .normal = undefined }); + vertex_data.appendAssumeCapacity(VertexData{ .position = F32x3{ x, -(height / 2.0), y }, .normal = undefined }); + } + + var group1: u32 = 1; + var group2: u32 = 3; + + for (0..num_sides) |_| { + if (group2 >= num_sides * 2) group2 = 1; + index_data.appendSliceAssumeCapacity(&[_]u32{ + 0, group1 + 1, group2 + 1, + group1 + 1, group1 + 2, group2 + 1, + group1 + 2, group2 + 2, group2 + 1, + group2 + 2, group1 + 2, 1, + }); + group1 += 2; + group2 += 2; + } + + { + var i: u32 = 0; + while (i < alloc_amt_idx) : (i += 3) { + const indexA: u32 = index_data.items[i]; + const indexB: u32 = index_data.items[i + 1]; + const indexC: u32 = index_data.items[i + 2]; + + const vert1: F32x4 = F32x4{ vertex_data.get(indexA).position[0], vertex_data.get(indexA).position[1], vertex_data.get(indexA).position[2], 1.0 }; + const vert2: F32x4 = F32x4{ vertex_data.get(indexB).position[0], vertex_data.get(indexB).position[1], vertex_data.get(indexB).position[2], 1.0 }; + const vert3: F32x4 = F32x4{ vertex_data.get(indexC).position[0], vertex_data.get(indexC).position[1], vertex_data.get(indexC).position[2], 1.0 }; + + const edgeAB: F32x4 = vert2 - vert1; + const edgeAC: F32x4 = vert3 - vert1; + + const cross = zmath.cross3(edgeAB, edgeAC); + + vertex_data.items(.normal)[indexA][0] += cross[0]; + vertex_data.items(.normal)[indexA][1] += cross[1]; + vertex_data.items(.normal)[indexA][2] += cross[2]; + vertex_data.items(.normal)[indexB][0] += cross[0]; + vertex_data.items(.normal)[indexB][1] += cross[1]; + vertex_data.items(.normal)[indexB][2] += cross[2]; + vertex_data.items(.normal)[indexC][0] += cross[0]; + vertex_data.items(.normal)[indexC][1] += cross[1]; + vertex_data.items(.normal)[indexC][2] += cross[2]; + } + } + + for (vertex_data.items(.position), vertex_data.items(.normal)) |pos, nor| { + out_vertex_data.appendAssumeCapacity(VertexData{ .position = pos, .normal = nor }); + } + + return Primitive{ .vertex_data = out_vertex_data, .vertex_count = alloc_amt_vert, .index_data = index_data, .index_count = alloc_amt_idx, .type = .cylinder }; +} + +pub fn createConePrimitive(allocator: std.mem.Allocator, radius: f32, height: f32, num_sides: u32) !Primitive { + const alloc_amt_vert: u32 = num_sides + 2; + const alloc_amt_idx: u32 = num_sides * 6; + + var vertex_data = VertexDataMAL{}; + try vertex_data.ensureTotalCapacity(allocator, alloc_amt_vert); + defer vertex_data.deinit(allocator); + + var out_vertex_data = try std.ArrayList(VertexData).initCapacity(allocator, alloc_amt_vert); + var index_data = try std.ArrayList(u32).initCapacity(allocator, alloc_amt_idx); + + vertex_data.appendAssumeCapacity(VertexData{ .position = F32x3{ 0.0, (height / 2.0), 0.0 }, .normal = undefined }); + vertex_data.appendAssumeCapacity(VertexData{ .position = F32x3{ 0.0, -(height / 2.0), 0.0 }, .normal = undefined }); + + const angle = 2.0 * PI / @as(f32, @floatFromInt(num_sides)); + + for (1..num_sides + 1) |i| { + const float_i = @as(f32, @floatFromInt(i)); + + const x: f32 = radius * zmath.sin(angle * float_i); + const y: f32 = radius * zmath.cos(angle * float_i); + + vertex_data.appendAssumeCapacity(VertexData{ .position = F32x3{ x, -(height / 2.0), y }, .normal = undefined }); + } + + var group1: u32 = 1; + var group2: u32 = 2; + + for (0..num_sides) |_| { + if (group2 >= num_sides + 1) group2 = 1; + index_data.appendSliceAssumeCapacity(&[_]u32{ + 0, group1 + 1, group2 + 1, + group2 + 1, group1 + 1, 1, + }); + group1 += 1; + group2 += 1; + } + + { + var i: u32 = 0; + while (i < alloc_amt_idx) : (i += 3) { + const indexA: u32 = index_data.items[i]; + const indexB: u32 = index_data.items[i + 1]; + const indexC: u32 = index_data.items[i + 2]; + + const vert1: F32x4 = F32x4{ vertex_data.get(indexA).position[0], vertex_data.get(indexA).position[1], vertex_data.get(indexA).position[2], 1.0 }; + const vert2: F32x4 = F32x4{ vertex_data.get(indexB).position[0], vertex_data.get(indexB).position[1], vertex_data.get(indexB).position[2], 1.0 }; + const vert3: F32x4 = F32x4{ vertex_data.get(indexC).position[0], vertex_data.get(indexC).position[1], vertex_data.get(indexC).position[2], 1.0 }; + + const edgeAB: F32x4 = vert2 - vert1; + const edgeAC: F32x4 = vert3 - vert1; + + const cross = zmath.cross3(edgeAB, edgeAC); + + vertex_data.items(.normal)[indexA][0] += cross[0]; + vertex_data.items(.normal)[indexA][1] += cross[1]; + vertex_data.items(.normal)[indexA][2] += cross[2]; + vertex_data.items(.normal)[indexB][0] += cross[0]; + vertex_data.items(.normal)[indexB][1] += cross[1]; + vertex_data.items(.normal)[indexB][2] += cross[2]; + vertex_data.items(.normal)[indexC][0] += cross[0]; + vertex_data.items(.normal)[indexC][1] += cross[1]; + vertex_data.items(.normal)[indexC][2] += cross[2]; + } + } + + for (vertex_data.items(.position), vertex_data.items(.normal)) |pos, nor| { + out_vertex_data.appendAssumeCapacity(VertexData{ .position = pos, .normal = nor }); + } + + return Primitive{ .vertex_data = out_vertex_data, .vertex_count = alloc_amt_vert, .index_data = index_data, .index_count = alloc_amt_idx, .type = .cone }; +} diff --git a/src/core/examples/procedural-primitives/renderer.zig b/src/core/examples/procedural-primitives/renderer.zig new file mode 100644 index 00000000..9fcd6df3 --- /dev/null +++ b/src/core/examples/procedural-primitives/renderer.zig @@ -0,0 +1,325 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const zm = @import("zmath"); +const primitives = @import("procedural-primitives.zig"); +const Primitive = primitives.Primitive; +const VertexData = primitives.VertexData; + +pub const Renderer = @This(); + +var queue: *gpu.Queue = undefined; +var pipeline: *gpu.RenderPipeline = undefined; +var app_timer: core.Timer = undefined; +var depth_texture: *gpu.Texture = undefined; +var depth_texture_view: *gpu.TextureView = undefined; + +const PrimitiveRenderData = struct { + vertex_buffer: *gpu.Buffer, + index_buffer: *gpu.Buffer, + vertex_count: u32, + index_count: u32, +}; + +const UniformBufferObject = struct { + mvp_matrix: zm.Mat, +}; +var uniform_buffer: *gpu.Buffer = undefined; +var bind_group: *gpu.BindGroup = undefined; + +var primitives_data: [7]PrimitiveRenderData = undefined; + +pub var curr_primitive_index: u4 = 0; + +pub fn init(allocator: std.mem.Allocator, timer: core.Timer) !void { + queue = core.queue; + app_timer = timer; + + { + const triangle_primitive = try primitives.createTrianglePrimitive(allocator, 1); + primitives_data[0] = PrimitiveRenderData{ .vertex_buffer = createVertexBuffer(triangle_primitive), .index_buffer = createIndexBuffer(triangle_primitive), .vertex_count = triangle_primitive.vertex_count, .index_count = triangle_primitive.index_count }; + defer triangle_primitive.vertex_data.deinit(); + defer triangle_primitive.index_data.deinit(); + } + + { + const quad_primitive = try primitives.createQuadPrimitive(allocator, 1.4); + primitives_data[1] = PrimitiveRenderData{ .vertex_buffer = createVertexBuffer(quad_primitive), .index_buffer = createIndexBuffer(quad_primitive), .vertex_count = quad_primitive.vertex_count, .index_count = quad_primitive.index_count }; + defer quad_primitive.vertex_data.deinit(); + defer quad_primitive.index_data.deinit(); + } + + { + const plane_primitive = try primitives.createPlanePrimitive(allocator, 1000, 1000, 1.5); + primitives_data[2] = PrimitiveRenderData{ .vertex_buffer = createVertexBuffer(plane_primitive), .index_buffer = createIndexBuffer(plane_primitive), .vertex_count = plane_primitive.vertex_count, .index_count = plane_primitive.index_count }; + defer plane_primitive.vertex_data.deinit(); + defer plane_primitive.index_data.deinit(); + } + + { + const circle_primitive = try primitives.createCirclePrimitive(allocator, 64, 1); + primitives_data[3] = PrimitiveRenderData{ .vertex_buffer = createVertexBuffer(circle_primitive), .index_buffer = createIndexBuffer(circle_primitive), .vertex_count = circle_primitive.vertex_count, .index_count = circle_primitive.index_count }; + defer circle_primitive.vertex_data.deinit(); + defer circle_primitive.index_data.deinit(); + } + + { + const cube_primitive = try primitives.createCubePrimitive(allocator, 0.5); + primitives_data[4] = PrimitiveRenderData{ .vertex_buffer = createVertexBuffer(cube_primitive), .index_buffer = createIndexBuffer(cube_primitive), .vertex_count = cube_primitive.vertex_count, .index_count = cube_primitive.index_count }; + defer cube_primitive.vertex_data.deinit(); + defer cube_primitive.index_data.deinit(); + } + + { + const cylinder_primitive = try primitives.createCylinderPrimitive(allocator, 1.0, 1.0, 6); + primitives_data[5] = PrimitiveRenderData{ .vertex_buffer = createVertexBuffer(cylinder_primitive), .index_buffer = createIndexBuffer(cylinder_primitive), .vertex_count = cylinder_primitive.vertex_count, .index_count = cylinder_primitive.index_count }; + defer cylinder_primitive.vertex_data.deinit(); + defer cylinder_primitive.index_data.deinit(); + } + + { + const cone_primitive = try primitives.createConePrimitive(allocator, 0.7, 1.0, 15); + primitives_data[6] = PrimitiveRenderData{ .vertex_buffer = createVertexBuffer(cone_primitive), .index_buffer = createIndexBuffer(cone_primitive), .vertex_count = cone_primitive.vertex_count, .index_count = cone_primitive.index_count }; + defer cone_primitive.vertex_data.deinit(); + defer cone_primitive.index_data.deinit(); + } + var bind_group_layout = createBindGroupLayout(); + defer bind_group_layout.release(); + + createBindBuffer(bind_group_layout); + + createDepthTexture(); + + var shader = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); + defer shader.release(); + + pipeline = createPipeline(shader, bind_group_layout); +} + +fn createVertexBuffer(primitive: Primitive) *gpu.Buffer { + const vertex_buffer_descriptor = gpu.Buffer.Descriptor{ + .size = primitive.vertex_count * @sizeOf(VertexData), + .usage = .{ .vertex = true, .copy_dst = true }, + .mapped_at_creation = .false, + }; + + const vertex_buffer = core.device.createBuffer(&vertex_buffer_descriptor); + queue.writeBuffer(vertex_buffer, 0, primitive.vertex_data.items[0..]); + + return vertex_buffer; +} + +fn createIndexBuffer(primitive: Primitive) *gpu.Buffer { + const index_buffer_descriptor = gpu.Buffer.Descriptor{ + .size = primitive.index_count * @sizeOf(u32), + .usage = .{ .index = true, .copy_dst = true }, + .mapped_at_creation = .false, + }; + const index_buffer = core.device.createBuffer(&index_buffer_descriptor); + queue.writeBuffer(index_buffer, 0, primitive.index_data.items[0..]); + + return index_buffer; +} + +fn createBindGroupLayout() *gpu.BindGroupLayout { + const bgle = gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true, .fragment = false }, .uniform, true, 0); + return core.device.createBindGroupLayout( + &gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &.{bgle}, + }), + ); +} + +fn createBindBuffer(bind_group_layout: *gpu.BindGroupLayout) void { + uniform_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = @sizeOf(UniformBufferObject), + .mapped_at_creation = .false, + }); + + bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bind_group_layout, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, uniform_buffer, 0, @sizeOf(UniformBufferObject)), + }, + }), + ); +} + +fn createDepthTexture() void { + depth_texture = core.device.createTexture(&gpu.Texture.Descriptor{ + .usage = .{ .render_attachment = true }, + .size = .{ .width = core.descriptor.width, .height = core.descriptor.height }, + .format = .depth24_plus, + }); + + depth_texture_view = depth_texture.createView(&gpu.TextureView.Descriptor{ + .format = .depth24_plus, + .dimension = .dimension_2d, + .array_layer_count = 1, + .mip_level_count = 1, + }); +} + +fn createPipeline(shader_module: *gpu.ShaderModule, bind_group_layout: *gpu.BindGroupLayout) *gpu.RenderPipeline { + const vertex_attributes = [_]gpu.VertexAttribute{ + .{ .format = .float32x3, .shader_location = 0, .offset = 0 }, + .{ .format = .float32x3, .shader_location = 1, .offset = @sizeOf(primitives.F32x3) }, + }; + + const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{ + .array_stride = @sizeOf(VertexData), + .step_mode = .vertex, + .attributes = &vertex_attributes, + }); + + const vertex_pipeline_state = gpu.VertexState.init(.{ .module = shader_module, .entry_point = "vertex_main", .buffers = &.{vertex_buffer_layout} }); + + const primitive_pipeline_state = gpu.PrimitiveState{ + .topology = .triangle_list, + .front_face = .ccw, + .cull_mode = .back, + }; + + // Fragment Pipeline State + const blend = gpu.BlendState{ + .color = gpu.BlendComponent{ .operation = .add, .src_factor = .src_alpha, .dst_factor = .one_minus_src_alpha }, + .alpha = gpu.BlendComponent{ .operation = .add, .src_factor = .zero, .dst_factor = .one }, + }; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment_pipeline_state = gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + + const depth_stencil_state = gpu.DepthStencilState{ + .format = .depth24_plus, + .depth_write_enabled = .true, + .depth_compare = .less, + }; + + const multi_sample_state = gpu.MultisampleState{ + .count = 1, + .mask = 0xFFFFFFFF, + .alpha_to_coverage_enabled = .false, + }; + + const bind_group_layouts = [_]*gpu.BindGroupLayout{bind_group_layout}; + // Pipeline Layout + const pipeline_layout_descriptor = gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &bind_group_layouts, + }); + const pipeline_layout = core.device.createPipelineLayout(&pipeline_layout_descriptor); + defer pipeline_layout.release(); + + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .label = "Main Pipeline", + .layout = pipeline_layout, + .vertex = vertex_pipeline_state, + .primitive = primitive_pipeline_state, + .depth_stencil = &depth_stencil_state, + .multisample = multi_sample_state, + .fragment = &fragment_pipeline_state, + }; + + return core.device.createRenderPipeline(&pipeline_descriptor); +} + +pub const F32x1 = @Vector(1, f32); + +pub fn update() void { + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = gpu.Color{ .r = 0.2, .g = 0.2, .b = 0.2, .a = 1.0 }, + .load_op = .clear, + .store_op = .store, + }; + + const depth_stencil_attachment = gpu.RenderPassDepthStencilAttachment{ + .view = depth_texture_view, + .depth_load_op = .clear, + .depth_store_op = .store, + .depth_clear_value = 1.0, + }; + + const encoder = core.device.createCommandEncoder(null); + const render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + .depth_stencil_attachment = &depth_stencil_attachment, + }); + + if (curr_primitive_index >= 4) { + const time = app_timer.read() / 5; + const model = zm.mul(zm.rotationX(time * (std.math.pi / 2.0)), zm.rotationZ(time * (std.math.pi / 2.0))); + const view = zm.lookAtRh( + zm.Vec{ 0, 4, 2, 1 }, + zm.Vec{ 0, 0, 0, 1 }, + zm.Vec{ 0, 0, 1, 0 }, + ); + const proj = zm.perspectiveFovRh( + (std.math.pi / 4.0), + @as(f32, @floatFromInt(core.descriptor.width)) / @as(f32, @floatFromInt(core.descriptor.height)), + 0.1, + 10, + ); + + const mvp = zm.mul(zm.mul(model, view), proj); + + const ubo = UniformBufferObject{ + .mvp_matrix = zm.transpose(mvp), + }; + encoder.writeBuffer(uniform_buffer, 0, &[_]UniformBufferObject{ubo}); + } else { + const ubo = UniformBufferObject{ + .mvp_matrix = zm.identity(), + }; + encoder.writeBuffer(uniform_buffer, 0, &[_]UniformBufferObject{ubo}); + } + + const pass = encoder.beginRenderPass(&render_pass_info); + + pass.setPipeline(pipeline); + + const vertex_buffer = primitives_data[curr_primitive_index].vertex_buffer; + const vertex_count = primitives_data[curr_primitive_index].vertex_count; + pass.setVertexBuffer(0, vertex_buffer, 0, @sizeOf(VertexData) * vertex_count); + + pass.setBindGroup(0, bind_group, &.{0}); + + const index_buffer = primitives_data[curr_primitive_index].index_buffer; + const index_count = primitives_data[curr_primitive_index].index_count; + pass.setIndexBuffer(index_buffer, .uint32, 0, @sizeOf(u32) * index_count); + pass.drawIndexed(index_count, 1, 0, 0, 0); + + pass.end(); + pass.release(); + + var command = encoder.finish(null); + encoder.release(); + + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); +} + +pub fn deinit() void { + var i: u4 = 0; + while (i < 7) : (i += 1) { + primitives_data[i].vertex_buffer.release(); + primitives_data[i].index_buffer.release(); + } + + bind_group.release(); + uniform_buffer.release(); + depth_texture.release(); + depth_texture_view.release(); + pipeline.release(); +} diff --git a/src/core/examples/procedural-primitives/shader.wgsl b/src/core/examples/procedural-primitives/shader.wgsl new file mode 100644 index 00000000..5370c204 --- /dev/null +++ b/src/core/examples/procedural-primitives/shader.wgsl @@ -0,0 +1,33 @@ +struct Uniforms { + mvp_matrix : mat4x4, +}; + +@binding(0) @group(0) var ubo : Uniforms; + +struct VertexInput { + @location(0) position: vec3, + @location(1) normal: vec3, +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) normal: vec3, +}; + +@vertex fn vertex_main(in : VertexInput) -> VertexOutput { + var out: VertexOutput; + out.position = vec4(in.position, 1.0) * ubo.mvp_matrix; + out.normal = in.normal; + return out; +} + +struct FragmentOutput { + @location(0) pixel_color: vec4 +}; + +@fragment fn frag_main(in: VertexOutput) -> FragmentOutput { + var out : FragmentOutput; + + out.pixel_color = vec4((in.normal + 1) / 2, 1.0); + return out; +} \ No newline at end of file diff --git a/src/core/examples/rgb-quad/main.zig b/src/core/examples/rgb-quad/main.zig new file mode 100644 index 00000000..7cd36fa3 --- /dev/null +++ b/src/core/examples/rgb-quad/main.zig @@ -0,0 +1,138 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; + +const Vertex = extern struct { + pos: @Vector(2, f32), + col: @Vector(3, f32), +}; +const vertices = [_]Vertex{ + .{ .pos = .{ -0.5, -0.5 }, .col = .{ 1, 0, 0 } }, + .{ .pos = .{ 0.5, -0.5 }, .col = .{ 0, 1, 0 } }, + .{ .pos = .{ 0.5, 0.5 }, .col = .{ 0, 0, 1 } }, + .{ .pos = .{ -0.5, 0.5 }, .col = .{ 1, 1, 1 } }, +}; +const index_data = [_]u32{ 0, 1, 2, 2, 3, 0 }; + +pub const App = @This(); + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +title_timer: core.Timer, +pipeline: *gpu.RenderPipeline, +vertex_buffer: *gpu.Buffer, +index_buffer: *gpu.Buffer, + +pub fn init(app: *App) !void { + try core.init(.{}); + + const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); + defer shader_module.release(); + + const vertex_attributes = [_]gpu.VertexAttribute{ + .{ .format = .float32x2, .offset = @offsetOf(Vertex, "pos"), .shader_location = 0 }, + .{ .format = .float32x3, .offset = @offsetOf(Vertex, "col"), .shader_location = 1 }, + }; + const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{ + .array_stride = @sizeOf(Vertex), + .step_mode = .vertex, + .attributes = &vertex_attributes, + }); + + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &.{}, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + + const pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{})); + defer pipeline_layout.release(); + + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + .layout = pipeline_layout, + .vertex = gpu.VertexState.init(.{ + .module = shader_module, + .entry_point = "vertex_main", + .buffers = &.{vertex_buffer_layout}, + }), + .primitive = .{ .cull_mode = .back }, + }; + + const vertex_buffer = core.device.createBuffer(&.{ + .usage = .{ .vertex = true }, + .size = @sizeOf(Vertex) * vertices.len, + .mapped_at_creation = .true, + }); + const vertex_mapped = vertex_buffer.getMappedRange(Vertex, 0, vertices.len); + @memcpy(vertex_mapped.?, vertices[0..]); + vertex_buffer.unmap(); + + const index_buffer = core.device.createBuffer(&.{ + .usage = .{ .index = true }, + .size = @sizeOf(u32) * index_data.len, + .mapped_at_creation = .true, + }); + const index_mapped = index_buffer.getMappedRange(u32, 0, index_data.len); + @memcpy(index_mapped.?, index_data[0..]); + index_buffer.unmap(); + + app.title_timer = try core.Timer.start(); + app.pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + app.vertex_buffer = vertex_buffer; + app.index_buffer = index_buffer; +} + +pub fn deinit(app: *App) void { + app.vertex_buffer.release(); + app.index_buffer.release(); + app.pipeline.release(); + core.deinit(); + _ = gpa.deinit(); +} + +pub fn update(app: *App) !bool { + var iter = core.pollEvents(); + while (iter.next()) |event| if (event == .close) return true; + + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const encoder = core.device.createCommandEncoder(null); + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = .{ .r = 0, .g = 0, .b = 0, .a = 1 }, + .load_op = .clear, + .store_op = .store, + }; + const render_pass_info = gpu.RenderPassDescriptor.init(.{ .color_attachments = &.{color_attachment} }); + + const pass = encoder.beginRenderPass(&render_pass_info); + pass.setPipeline(app.pipeline); + pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len); + pass.setIndexBuffer(app.index_buffer, .uint32, 0, @sizeOf(u32) * index_data.len); + pass.drawIndexed(index_data.len, 1, 0, 0, 0); + pass.end(); + pass.release(); + + var command = encoder.finish(null); + encoder.release(); + core.queue.submit(&.{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("RGB Quad [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} diff --git a/src/core/examples/rgb-quad/shader.wgsl b/src/core/examples/rgb-quad/shader.wgsl new file mode 100644 index 00000000..85f687bc --- /dev/null +++ b/src/core/examples/rgb-quad/shader.wgsl @@ -0,0 +1,15 @@ +struct Output { + @builtin(position) pos: vec4, + @location(0) color: vec3, +}; + +@vertex fn vertex_main(@location(0) pos: vec2, @location(1) color: vec3) -> Output { + var output: Output; + output.pos = vec4(pos, 0, 1); + output.color = color; + return output; +} + +@fragment fn frag_main(@location(0) color: vec3) -> @location(0) vec4 { + return vec4(color, 1); +} \ No newline at end of file diff --git a/src/core/examples/rotating-cube/cube_mesh.zig b/src/core/examples/rotating-cube/cube_mesh.zig new file mode 100644 index 00000000..f26c75ac --- /dev/null +++ b/src/core/examples/rotating-cube/cube_mesh.zig @@ -0,0 +1,49 @@ +pub const Vertex = extern struct { + pos: @Vector(4, f32), + col: @Vector(4, f32), + uv: @Vector(2, f32), +}; + +pub const vertices = [_]Vertex{ + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, +}; diff --git a/src/core/examples/rotating-cube/main.zig b/src/core/examples/rotating-cube/main.zig new file mode 100755 index 00000000..bc4e4e77 --- /dev/null +++ b/src/core/examples/rotating-cube/main.zig @@ -0,0 +1,192 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const zm = @import("zmath"); +const Vertex = @import("cube_mesh.zig").Vertex; +const vertices = @import("cube_mesh.zig").vertices; + +pub const App = @This(); + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; +const UniformBufferObject = struct { + mat: zm.Mat, +}; + +title_timer: core.Timer, +timer: core.Timer, +pipeline: *gpu.RenderPipeline, +vertex_buffer: *gpu.Buffer, +uniform_buffer: *gpu.Buffer, +bind_group: *gpu.BindGroup, + +pub fn init(app: *App) !void { + try core.init(.{}); + + const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); + + const vertex_attributes = [_]gpu.VertexAttribute{ + .{ .format = .float32x4, .offset = @offsetOf(Vertex, "pos"), .shader_location = 0 }, + .{ .format = .float32x2, .offset = @offsetOf(Vertex, "uv"), .shader_location = 1 }, + }; + const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{ + .array_stride = @sizeOf(Vertex), + .step_mode = .vertex, + .attributes = &vertex_attributes, + }); + + const blend = gpu.BlendState{}; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + + const bgle = gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, true, 0); + const bgl = core.device.createBindGroupLayout( + &gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &.{bgle}, + }), + ); + + const bind_group_layouts = [_]*gpu.BindGroupLayout{bgl}; + const pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &bind_group_layouts, + })); + + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + .layout = pipeline_layout, + .vertex = gpu.VertexState.init(.{ + .module = shader_module, + .entry_point = "vertex_main", + .buffers = &.{vertex_buffer_layout}, + }), + .primitive = .{ + .cull_mode = .back, + }, + }; + + const vertex_buffer = core.device.createBuffer(&.{ + .usage = .{ .vertex = true }, + .size = @sizeOf(Vertex) * vertices.len, + .mapped_at_creation = .true, + }); + const vertex_mapped = vertex_buffer.getMappedRange(Vertex, 0, vertices.len); + @memcpy(vertex_mapped.?, vertices[0..]); + vertex_buffer.unmap(); + + const uniform_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = @sizeOf(UniformBufferObject), + .mapped_at_creation = .false, + }); + const bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bgl, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, uniform_buffer, 0, @sizeOf(UniformBufferObject)), + }, + }), + ); + + app.title_timer = try core.Timer.start(); + app.timer = try core.Timer.start(); + app.pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + app.vertex_buffer = vertex_buffer; + app.uniform_buffer = uniform_buffer; + app.bind_group = bind_group; + + shader_module.release(); + pipeline_layout.release(); + bgl.release(); +} + +pub fn deinit(app: *App) void { + defer _ = gpa.deinit(); + defer core.deinit(); + + app.vertex_buffer.release(); + app.uniform_buffer.release(); + app.bind_group.release(); + app.pipeline.release(); +} + +pub fn update(app: *App) !bool { + var iter = core.pollEvents(); + while (iter.next()) |event| { + switch (event) { + .key_press => |ev| { + if (ev.key == .space) return true; + }, + .close => return true, + else => {}, + } + } + + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = std.mem.zeroes(gpu.Color), + .load_op = .clear, + .store_op = .store, + }; + + const queue = core.queue; + const encoder = core.device.createCommandEncoder(null); + const render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + }); + + { + const time = app.timer.read(); + const model = zm.mul(zm.rotationX(time * (std.math.pi / 2.0)), zm.rotationZ(time * (std.math.pi / 2.0))); + const view = zm.lookAtRh( + zm.Vec{ 0, 4, 2, 1 }, + zm.Vec{ 0, 0, 0, 1 }, + zm.Vec{ 0, 0, 1, 0 }, + ); + const proj = zm.perspectiveFovRh( + (std.math.pi / 4.0), + @as(f32, @floatFromInt(core.descriptor.width)) / @as(f32, @floatFromInt(core.descriptor.height)), + 0.1, + 10, + ); + const mvp = zm.mul(zm.mul(model, view), proj); + const ubo = UniformBufferObject{ + .mat = zm.transpose(mvp), + }; + queue.writeBuffer(app.uniform_buffer, 0, &[_]UniformBufferObject{ubo}); + } + + const pass = encoder.beginRenderPass(&render_pass_info); + pass.setPipeline(app.pipeline); + pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len); + pass.setBindGroup(0, app.bind_group, &.{0}); + pass.draw(vertices.len, 1, 0, 0); + pass.end(); + pass.release(); + + var command = encoder.finish(null); + encoder.release(); + + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Rotating Cube [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} diff --git a/src/core/examples/rotating-cube/shader.wgsl b/src/core/examples/rotating-cube/shader.wgsl new file mode 100644 index 00000000..6b7291ee --- /dev/null +++ b/src/core/examples/rotating-cube/shader.wgsl @@ -0,0 +1,24 @@ +@group(0) @binding(0) var ubo : mat4x4; +struct VertexOut { + @builtin(position) position_clip : vec4, + @location(0) fragUV : vec2, + @location(1) fragPosition: vec4, +} + +@vertex fn vertex_main( + @location(0) position : vec4, + @location(1) uv: vec2 +) -> VertexOut { + var output : VertexOut; + output.position_clip = position * ubo; + output.fragUV = uv; + output.fragPosition = 0.5 * (position + vec4(1.0, 1.0, 1.0, 1.0)); + return output; +} + +@fragment fn frag_main( + @location(0) fragUV: vec2, + @location(1) fragPosition: vec4 +) -> @location(0) vec4 { + return fragPosition; +} \ No newline at end of file diff --git a/src/core/examples/sprite2d/main.zig b/src/core/examples/sprite2d/main.zig new file mode 100644 index 00000000..cddc0447 --- /dev/null +++ b/src/core/examples/sprite2d/main.zig @@ -0,0 +1,354 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const zm = @import("zmath"); +const zigimg = @import("zigimg"); +const assets = @import("assets"); +const json = std.json; + +pub const App = @This(); + +const speed = 2.0 * 100.0; // pixels per second + +const Vec2 = @Vector(2, f32); + +const UniformBufferObject = struct { + mat: zm.Mat, +}; +const Sprite = extern struct { + pos: Vec2, + size: Vec2, + world_pos: Vec2, + sheet_size: Vec2, +}; +const SpriteFrames = extern struct { + up: Vec2, + down: Vec2, + left: Vec2, + right: Vec2, +}; +const JSONFrames = struct { + up: []f32, + down: []f32, + left: []f32, + right: []f32, +}; +const JSONSprite = struct { + pos: []f32, + size: []f32, + world_pos: []f32, + is_player: bool = false, + frames: JSONFrames, +}; +const SpriteSheet = struct { + width: f32, + height: f32, +}; +const JSONData = struct { + sheet: SpriteSheet, + sprites: []JSONSprite, +}; +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +title_timer: core.Timer, +timer: core.Timer, +fps_timer: core.Timer, +pipeline: *gpu.RenderPipeline, +uniform_buffer: *gpu.Buffer, +bind_group: *gpu.BindGroup, +sheet: SpriteSheet, +sprites_buffer: *gpu.Buffer, +sprites: std.ArrayList(Sprite), +sprites_frames: std.ArrayList(SpriteFrames), +player_pos: Vec2, +direction: Vec2, +player_sprite_index: usize, + +pub fn init(app: *App) !void { + try core.init(.{}); + + const allocator = gpa.allocator(); + + const sprites_file = try std.fs.cwd().openFile("../../examples/sprite2d/sprites.json", .{ .mode = .read_only }); + defer sprites_file.close(); + const file_size = (try sprites_file.stat()).size; + const buffer = try allocator.alloc(u8, file_size); + defer allocator.free(buffer); + try sprites_file.reader().readNoEof(buffer); + const root = try std.json.parseFromSlice(JSONData, allocator, buffer, .{}); + defer root.deinit(); + + app.player_pos = Vec2{ 0, 0 }; + app.direction = Vec2{ 0, 0 }; + app.sheet = root.value.sheet; + std.log.info("Sheet Dimensions: {} {}", .{ app.sheet.width, app.sheet.height }); + app.sprites = std.ArrayList(Sprite).init(allocator); + app.sprites_frames = std.ArrayList(SpriteFrames).init(allocator); + for (root.value.sprites) |sprite| { + std.log.info("Sprite World Position: {} {}", .{ sprite.world_pos[0], sprite.world_pos[1] }); + std.log.info("Sprite Texture Position: {} {}", .{ sprite.pos[0], sprite.pos[1] }); + std.log.info("Sprite Dimensions: {} {}", .{ sprite.size[0], sprite.size[1] }); + if (sprite.is_player) { + app.player_sprite_index = app.sprites.items.len; + } + try app.sprites.append(.{ + .pos = Vec2{ sprite.pos[0], sprite.pos[1] }, + .size = Vec2{ sprite.size[0], sprite.size[1] }, + .world_pos = Vec2{ sprite.world_pos[0], sprite.world_pos[1] }, + .sheet_size = Vec2{ app.sheet.width, app.sheet.height }, + }); + try app.sprites_frames.append(.{ .up = Vec2{ sprite.frames.up[0], sprite.frames.up[1] }, .down = Vec2{ sprite.frames.down[0], sprite.frames.down[1] }, .left = Vec2{ sprite.frames.left[0], sprite.frames.left[1] }, .right = Vec2{ sprite.frames.right[0], sprite.frames.right[1] } }); + } + std.log.info("Number of sprites: {}", .{app.sprites.items.len}); + + const shader_module = core.device.createShaderModuleWGSL("sprite-shader.wgsl", @embedFile("sprite-shader.wgsl")); + + const blend = gpu.BlendState{ + .color = .{ + .operation = .add, + .src_factor = .src_alpha, + .dst_factor = .one_minus_src_alpha, + }, + .alpha = .{ + .operation = .add, + .src_factor = .one, + .dst_factor = .zero, + }, + }; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + .vertex = gpu.VertexState.init(.{ + .module = shader_module, + .entry_point = "vertex_main", + }), + }; + const pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + + const sprites_buffer = core.device.createBuffer(&.{ + .usage = .{ .storage = true, .copy_dst = true }, + .size = @sizeOf(Sprite) * app.sprites.items.len, + .mapped_at_creation = .true, + }); + const sprites_mapped = sprites_buffer.getMappedRange(Sprite, 0, app.sprites.items.len); + @memcpy(sprites_mapped.?, app.sprites.items[0..]); + sprites_buffer.unmap(); + + // Create a sampler with linear filtering for smooth interpolation. + const sampler = core.device.createSampler(&.{ + .mag_filter = .linear, + .min_filter = .linear, + }); + const queue = core.queue; + var img = try zigimg.Image.fromMemory(allocator, assets.sprites_sheet_png); + defer img.deinit(); + const img_size = gpu.Extent3D{ .width = @as(u32, @intCast(img.width)), .height = @as(u32, @intCast(img.height)) }; + std.log.info("Image Dimensions: {} {}", .{ img.width, img.height }); + const texture = core.device.createTexture(&.{ + .size = img_size, + .format = .rgba8_unorm, + .usage = .{ + .texture_binding = true, + .copy_dst = true, + .render_attachment = true, + }, + }); + const data_layout = gpu.Texture.DataLayout{ + .bytes_per_row = @as(u32, @intCast(img.width * 4)), + .rows_per_image = @as(u32, @intCast(img.height)), + }; + switch (img.pixels) { + .rgba32 => |pixels| queue.writeTexture(&.{ .texture = texture }, &data_layout, &img_size, pixels), + .rgb24 => |pixels| { + const data = try rgb24ToRgba32(allocator, pixels); + defer data.deinit(allocator); + queue.writeTexture(&.{ .texture = texture }, &data_layout, &img_size, data.rgba32); + }, + else => @panic("unsupported image color format"), + } + + const uniform_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = @sizeOf(UniformBufferObject), + .mapped_at_creation = .false, + }); + + const texture_view = texture.createView(&gpu.TextureView.Descriptor{}); + texture.release(); + + const bind_group_layout = pipeline.getBindGroupLayout(0); + const bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bind_group_layout, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, uniform_buffer, 0, @sizeOf(UniformBufferObject)), + gpu.BindGroup.Entry.sampler(1, sampler), + gpu.BindGroup.Entry.textureView(2, texture_view), + gpu.BindGroup.Entry.buffer(3, sprites_buffer, 0, @sizeOf(Sprite) * app.sprites.items.len), + }, + }), + ); + texture_view.release(); + sampler.release(); + bind_group_layout.release(); + + app.title_timer = try core.Timer.start(); + app.timer = try core.Timer.start(); + app.fps_timer = try core.Timer.start(); + app.pipeline = pipeline; + app.uniform_buffer = uniform_buffer; + app.bind_group = bind_group; + app.sprites_buffer = sprites_buffer; + + shader_module.release(); +} + +pub fn deinit(app: *App) void { + defer _ = gpa.deinit(); + defer core.deinit(); + + app.pipeline.release(); + app.sprites.deinit(); + app.sprites_frames.deinit(); + app.uniform_buffer.release(); + app.bind_group.release(); + app.sprites_buffer.release(); +} + +pub fn update(app: *App) !bool { + // Handle input by determining the direction the player wants to go. + var iter = core.pollEvents(); + while (iter.next()) |event| { + switch (event) { + .key_press => |ev| { + switch (ev.key) { + .space => return true, + .left => app.direction[0] += 1, + .right => app.direction[0] -= 1, + .up => app.direction[1] += 1, + .down => app.direction[1] -= 1, + else => {}, + } + }, + .key_release => |ev| { + switch (ev.key) { + .left => app.direction[0] -= 1, + .right => app.direction[0] += 1, + .up => app.direction[1] -= 1, + .down => app.direction[1] += 1, + else => {}, + } + }, + .close => return true, + else => {}, + } + } + + // Calculate the player position, by moving in the direction the player wants to go + // by the speed amount. Multiply by delta_time to ensure that movement is the same speed + // regardless of the frame rate. + const delta_time = app.fps_timer.lap(); + app.player_pos += app.direction * Vec2{ speed, speed } * Vec2{ delta_time, delta_time }; + + // Render the frame + try app.render(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Sprite2D [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} + +fn render(app: *App) !void { + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + // sky blue background color: + .clear_value = .{ .r = 0.52, .g = 0.8, .b = 0.92, .a = 1.0 }, + .load_op = .clear, + .store_op = .store, + }; + + const encoder = core.device.createCommandEncoder(null); + const render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + }); + + const player_sprite = &app.sprites.items[app.player_sprite_index]; + const player_sprite_frame = &app.sprites_frames.items[app.player_sprite_index]; + if (app.direction[0] == -1.0) { + player_sprite.pos = player_sprite_frame.left; + } else if (app.direction[0] == 1.0) { + player_sprite.pos = player_sprite_frame.right; + } else if (app.direction[1] == -1.0) { + player_sprite.pos = player_sprite_frame.down; + } else if (app.direction[1] == 1.0) { + player_sprite.pos = player_sprite_frame.up; + } + player_sprite.world_pos = app.player_pos; + + // One pixel in our scene will equal one window pixel (i.e. be roughly the same size + // irrespective of whether the user has a Retina/HDPI display.) + const proj = zm.orthographicRh( + @as(f32, @floatFromInt(core.size().width)), + @as(f32, @floatFromInt(core.size().height)), + 0.1, + 1000, + ); + const view = zm.lookAtRh( + zm.Vec{ 0, 1000, 0, 1 }, + zm.Vec{ 0, 0, 0, 1 }, + zm.Vec{ 0, 0, 1, 0 }, + ); + const mvp = zm.mul(view, proj); + const ubo = UniformBufferObject{ + .mat = zm.transpose(mvp), + }; + + // Pass the latest uniform values & sprite values to the shader program. + encoder.writeBuffer(app.uniform_buffer, 0, &[_]UniformBufferObject{ubo}); + encoder.writeBuffer(app.sprites_buffer, 0, app.sprites.items); + + // Draw the sprite batch + const total_vertices = @as(u32, @intCast(app.sprites.items.len * 6)); + const pass = encoder.beginRenderPass(&render_pass_info); + pass.setPipeline(app.pipeline); + pass.setBindGroup(0, app.bind_group, &.{}); + pass.draw(total_vertices, 1, 0, 0); + pass.end(); + pass.release(); + + // Submit the frame. + var command = encoder.finish(null); + encoder.release(); + const queue = core.queue; + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); +} + +fn rgb24ToRgba32(allocator: std.mem.Allocator, in: []zigimg.color.Rgb24) !zigimg.color.PixelStorage { + const out = try zigimg.color.PixelStorage.init(allocator, .rgba32, in.len); + var i: usize = 0; + while (i < in.len) : (i += 1) { + out.rgba32[i] = zigimg.color.Rgba32{ .r = in[i].r, .g = in[i].g, .b = in[i].b, .a = 255 }; + } + return out; +} diff --git a/src/core/examples/sprite2d/sprite-shader.wgsl b/src/core/examples/sprite2d/sprite-shader.wgsl new file mode 100644 index 00000000..979c14de --- /dev/null +++ b/src/core/examples/sprite2d/sprite-shader.wgsl @@ -0,0 +1,82 @@ +struct Uniforms { + modelViewProjectionMatrix : mat4x4, +}; +@binding(0) @group(0) var uniforms : Uniforms; + +struct VertexOutput { + @builtin(position) Position : vec4, + @location(0) fragUV : vec2, + @location(1) spriteIndex : f32, +}; + +struct Sprite { + pos: vec2, + size: vec2, + world_pos: vec2, + sheet_size: vec2, +}; +@binding(3) @group(0) var sprites: array; + +@vertex +fn vertex_main( + @builtin(vertex_index) VertexIndex : u32 +) -> VertexOutput { + var sprite = sprites[VertexIndex / 6]; + + // Calculate the vertex position + var positions = array, 6>( + vec2(0.0, 0.0), // bottom-left + vec2(0.0, 1.0), // top-left + vec2(1.0, 0.0), // bottom-right + vec2(1.0, 0.0), // bottom-right + vec2(0.0, 1.0), // top-left + vec2(1.0, 1.0), // top-right + ); + var pos = positions[VertexIndex % 6]; + pos.x *= sprite.size.x; + pos.y *= sprite.size.y; + pos.x += sprite.world_pos.x; + pos.y += sprite.world_pos.y; + + // Calculate the UV coordinate + var uvs = array, 6>( + vec2(0.0, 1.0), // bottom-left + vec2(0.0, 0.0), // top-left + vec2(1.0, 1.0), // bottom-right + vec2(1.0, 1.0), // bottom-right + vec2(0.0, 0.0), // top-left + vec2(1.0, 0.0), // top-right + ); + var uv = uvs[VertexIndex % 6]; + uv.x *= sprite.size.x / sprite.sheet_size.x; + uv.y *= sprite.size.y / sprite.sheet_size.y; + uv.x += sprite.pos.x / sprite.sheet_size.x; + uv.y += sprite.pos.y / sprite.sheet_size.y; + + var output : VertexOutput; + output.Position = vec4(pos.x, 0.0, pos.y, 1.0) * uniforms.modelViewProjectionMatrix; + output.fragUV = uv; + output.spriteIndex = f32(VertexIndex / 6); + return output; +} + +@group(0) @binding(1) var spriteSampler: sampler; +@group(0) @binding(2) var spriteTexture: texture_2d; + +@fragment +fn frag_main( + @location(0) fragUV: vec2, + @location(1) spriteIndex: f32 +) -> @location(0) vec4 { + var color = textureSample(spriteTexture, spriteSampler, fragUV); + if (spriteIndex == 0.0) { + if (color[3] > 0.0) { + color[0] = 0.3; + color[1] = 0.2; + color[2] = 0.5; + color[3] = 1.0; + } + } + + return color; +} \ No newline at end of file diff --git a/src/core/examples/sprite2d/sprites.json b/src/core/examples/sprite2d/sprites.json new file mode 100644 index 00000000..37d5ab13 --- /dev/null +++ b/src/core/examples/sprite2d/sprites.json @@ -0,0 +1,53 @@ +{ + "sheet": { + "width": 352.0, + "height": 32.0 + }, + "sprites": [ + { + "pos": [ 0.0, 0.0 ], + "size": [ 32.0, 32.0 ], + "world_pos": [ 0.0, 0.0 ], + "is_player": true, + "frames": { + "down": [ 0.0, 0.0 ], + "left": [ 32.0, 0.0 ], + "right": [ 64.0, 0.0 ], + "up": [ 96.0, 0.0 ] + } + }, + { + "pos": [ 128.0, 0.0 ], + "size": [ 32.0, 32.0 ], + "world_pos": [ 32.0, 32.0 ], + "frames": { + "down": [ 128.0, 0.0 ], + "left": [ 160.0, 0.0 ], + "right": [ 192.0, 0.0 ], + "up": [ 224.0, 0.0 ] + } + }, + { + "pos": [ 128.0, 0.0 ], + "size": [ 32.0, 32.0 ], + "world_pos": [ 64.0, 64.0 ], + "frames": { + "down": [ 0.0, 0.0 ], + "left": [ 0.0, 0.0 ], + "right": [ 0.0, 0.0 ], + "up": [ 0.0, 0.0 ] + } + }, + { + "pos": [ 256.0, 0.0 ], + "size": [ 32.0, 32.0 ], + "world_pos": [ 96.0, 96.0 ], + "frames": { + "down": [ 0.0, 0.0 ], + "left": [ 0.0, 0.0 ], + "right": [ 0.0, 0.0 ], + "up": [ 0.0, 0.0 ] + } + } + ] +} \ No newline at end of file diff --git a/src/core/examples/sysgpu/boids/main.zig b/src/core/examples/sysgpu/boids/main.zig new file mode 100644 index 00000000..bc5e80c6 --- /dev/null +++ b/src/core/examples/sysgpu/boids/main.zig @@ -0,0 +1,268 @@ +/// A port of Austin Eng's "computeBoids" webgpu sample. +/// https://github.com/austinEng/webgpu-samples/blob/main/src/sample/computeBoids/main.ts +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; + +title_timer: core.Timer, +timer: core.Timer, +compute_pipeline: *gpu.ComputePipeline, +render_pipeline: *gpu.RenderPipeline, +sprite_vertex_buffer: *gpu.Buffer, +particle_buffers: [2]*gpu.Buffer, +particle_bind_groups: [2]*gpu.BindGroup, +sim_param_buffer: *gpu.Buffer, +frame_counter: usize, + +pub const App = @This(); + +pub const mach_core_options = core.ComptimeOptions{ + .use_wgpu = false, + .use_sysgpu = true, +}; + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +const num_particle = 1500; + +var sim_params = [_]f32{ + 0.04, // .delta_T + 0.1, // .rule_1_distance + 0.025, // .rule_2_distance + 0.025, // .rule_3_distance + 0.02, // .rule_1_scale + 0.05, // .rule_2_scale + 0.005, // .rule_3_scale +}; + +pub fn init(app: *App) !void { + try core.init(.{}); + + const sprite_shader_module = core.device.createShaderModuleWGSL( + "sprite.wgsl", + @embedFile("sprite.wgsl"), + ); + defer sprite_shader_module.release(); + + const update_sprite_shader_module = core.device.createShaderModuleWGSL( + "updateSprites.wgsl", + @embedFile("updateSprites.wgsl"), + ); + defer update_sprite_shader_module.release(); + + const instanced_particles_attributes = [_]gpu.VertexAttribute{ + .{ + // instance position + .shader_location = 0, + .offset = 0, + .format = .float32x2, + }, + .{ + // instance velocity + .shader_location = 1, + .offset = 2 * 4, + .format = .float32x2, + }, + }; + + const vertex_buffer_attributes = [_]gpu.VertexAttribute{ + .{ + // vertex positions + .shader_location = 2, + .offset = 0, + .format = .float32x2, + }, + }; + + const render_pipeline = core.device.createRenderPipeline(&gpu.RenderPipeline.Descriptor{ + .vertex = gpu.VertexState.init(.{ + .module = sprite_shader_module, + .entry_point = "vert_main", + .buffers = &.{ + gpu.VertexBufferLayout.init(.{ + // instanced particles buffer + .array_stride = 4 * 4, + .step_mode = .instance, + .attributes = &instanced_particles_attributes, + }), + gpu.VertexBufferLayout.init(.{ + // vertex buffer + .array_stride = 2 * 4, + .step_mode = .vertex, + .attributes = &vertex_buffer_attributes, + }), + }, + }), + .fragment = &gpu.FragmentState.init(.{ + .module = sprite_shader_module, + .entry_point = "frag_main", + .targets = &[_]gpu.ColorTargetState{.{ + .format = core.descriptor.format, + }}, + }), + }); + + const compute_pipeline = core.device.createComputePipeline(&gpu.ComputePipeline.Descriptor{ .compute = gpu.ProgrammableStageDescriptor{ + .module = update_sprite_shader_module, + .entry_point = "main", + } }); + + const vert_buffer_data = [_]f32{ + -0.01, -0.02, 0.01, + -0.02, 0.0, 0.02, + }; + + const sprite_vertex_buffer = core.device.createBuffer(&gpu.Buffer.Descriptor{ + .label = "sprite_vertex_buffer", + .usage = .{ .vertex = true }, + .mapped_at_creation = .true, + .size = vert_buffer_data.len * @sizeOf(f32), + }); + const vertex_mapped = sprite_vertex_buffer.getMappedRange(f32, 0, vert_buffer_data.len); + @memcpy(vertex_mapped.?, vert_buffer_data[0..]); + sprite_vertex_buffer.unmap(); + + const sim_param_buffer = core.device.createBuffer(&gpu.Buffer.Descriptor{ + .label = "sim_param_buffer", + .usage = .{ .uniform = true, .copy_dst = true }, + .size = sim_params.len * @sizeOf(f32), + }); + core.queue.writeBuffer(sim_param_buffer, 0, sim_params[0..]); + + var initial_particle_data: [num_particle * 4]f32 = undefined; + var rng = std.rand.DefaultPrng.init(0); + const random = rng.random(); + var i: usize = 0; + while (i < num_particle) : (i += 1) { + initial_particle_data[4 * i + 0] = 2 * (random.float(f32) - 0.5); + initial_particle_data[4 * i + 1] = 2 * (random.float(f32) - 0.5); + initial_particle_data[4 * i + 2] = 2 * (random.float(f32) - 0.5) * 0.1; + initial_particle_data[4 * i + 3] = 2 * (random.float(f32) - 0.5) * 0.1; + } + + var particle_buffers: [2]*gpu.Buffer = undefined; + var particle_bind_groups: [2]*gpu.BindGroup = undefined; + i = 0; + while (i < 2) : (i += 1) { + particle_buffers[i] = core.device.createBuffer(&gpu.Buffer.Descriptor{ + .label = "particle_buffer", + .mapped_at_creation = .true, + .usage = .{ + .vertex = true, + .storage = true, + }, + .size = initial_particle_data.len * @sizeOf(f32), + }); + const mapped = particle_buffers[i].getMappedRange(f32, 0, initial_particle_data.len); + @memcpy(mapped.?, initial_particle_data[0..]); + particle_buffers[i].unmap(); + } + + i = 0; + while (i < 2) : (i += 1) { + const layout = compute_pipeline.getBindGroupLayout(0); + defer layout.release(); + + particle_bind_groups[i] = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{ + .layout = layout, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, sim_param_buffer, 0, sim_params.len * @sizeOf(f32), sim_params.len * @sizeOf(f32)), + gpu.BindGroup.Entry.buffer(1, particle_buffers[i], 0, initial_particle_data.len * @sizeOf(f32), 4 * @sizeOf(f32)), + gpu.BindGroup.Entry.buffer(2, particle_buffers[(i + 1) % 2], 0, initial_particle_data.len * @sizeOf(f32), 4 * @sizeOf(f32)), + }, + })); + } + + app.* = .{ + .timer = try core.Timer.start(), + .title_timer = try core.Timer.start(), + .compute_pipeline = compute_pipeline, + .render_pipeline = render_pipeline, + .sprite_vertex_buffer = sprite_vertex_buffer, + .particle_buffers = particle_buffers, + .particle_bind_groups = particle_bind_groups, + .sim_param_buffer = sim_param_buffer, + .frame_counter = 0, + }; +} + +pub fn deinit(app: *App) void { + defer _ = gpa.deinit(); + defer core.deinit(); + + app.compute_pipeline.release(); + app.render_pipeline.release(); + app.sprite_vertex_buffer.release(); + for (app.particle_buffers) |particle_buffer| particle_buffer.release(); + for (app.particle_bind_groups) |particle_bind_group| particle_bind_group.release(); + app.sim_param_buffer.release(); +} + +pub fn update(app: *App) !bool { + const delta_time = app.timer.lap(); + + var iter = core.pollEvents(); + while (iter.next()) |event| { + if (event == .close) return true; + } + + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = std.mem.zeroes(gpu.Color), + .load_op = .clear, + .store_op = .store, + }; + + const render_pass_descriptor = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{ + color_attachment, + }, + }); + + sim_params[0] = @as(f32, @floatCast(delta_time)); + core.queue.writeBuffer(app.sim_param_buffer, 0, sim_params[0..]); + + const command_encoder = core.device.createCommandEncoder(null); + { + const pass_encoder = command_encoder.beginComputePass(null); + pass_encoder.setPipeline(app.compute_pipeline); + pass_encoder.setBindGroup(0, app.particle_bind_groups[app.frame_counter % 2], null); + pass_encoder.dispatchWorkgroups(@as(u32, @intFromFloat(@ceil(@as(f32, num_particle) / 64))), 1, 1); + pass_encoder.end(); + pass_encoder.release(); + } + { + const pass_encoder = command_encoder.beginRenderPass(&render_pass_descriptor); + pass_encoder.setPipeline(app.render_pipeline); + pass_encoder.setVertexBuffer(0, app.particle_buffers[(app.frame_counter + 1) % 2], 0, num_particle * 4 * @sizeOf(f32)); + pass_encoder.setVertexBuffer(1, app.sprite_vertex_buffer, 0, 6 * @sizeOf(f32)); + pass_encoder.draw(3, num_particle, 0, 0); + pass_encoder.end(); + pass_encoder.release(); + } + + app.frame_counter += 1; + if (app.frame_counter % 60 == 0) { + std.log.info("Frame {}", .{app.frame_counter}); + } + + var command = command_encoder.finish(null); + command_encoder.release(); + core.queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + + core.swap_chain.present(); + back_buffer_view.release(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Boids [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} diff --git a/src/core/examples/sysgpu/boids/sprite.wgsl b/src/core/examples/sysgpu/boids/sprite.wgsl new file mode 100644 index 00000000..c97c5c18 --- /dev/null +++ b/src/core/examples/sysgpu/boids/sprite.wgsl @@ -0,0 +1,15 @@ +@vertex +fn vert_main(@location(0) a_particlePos : vec2, + @location(1) a_particleVel : vec2, + @location(2) a_pos : vec2) -> @builtin(position) vec4 { + let angle = -atan2(a_particleVel.x, a_particleVel.y); + let pos = vec2( + (a_pos.x * cos(angle)) - (a_pos.y * sin(angle)), + (a_pos.x * sin(angle)) + (a_pos.y * cos(angle))); + return vec4(pos + a_particlePos, 0.0, 1.0); +} + +@fragment +fn frag_main() -> @location(0) vec4 { + return vec4(1.0, 1.0, 1.0, 1.0); +} diff --git a/src/core/examples/sysgpu/boids/updateSprites.wgsl b/src/core/examples/sysgpu/boids/updateSprites.wgsl new file mode 100644 index 00000000..89071499 --- /dev/null +++ b/src/core/examples/sysgpu/boids/updateSprites.wgsl @@ -0,0 +1,90 @@ +struct Particle { + pos : vec2, + vel : vec2, +}; +struct SimParams { + deltaT : f32, + rule1Distance : f32, + rule2Distance : f32, + rule3Distance : f32, + rule1Scale : f32, + rule2Scale : f32, + rule3Scale : f32, +}; +struct Particles { + particles : array, +}; +@binding(0) @group(0) var params : SimParams; +@binding(1) @group(0) var particlesA : Particles; +@binding(2) @group(0) var particlesB : Particles; + +// https://github.com/austinEng/Project6-Vulkan-Flocking/blob/master/data/shaders/computeparticles/particle.comp +@compute @workgroup_size(64) +fn main(@builtin(global_invocation_id) GlobalInvocationID : vec3) { + var index : u32 = GlobalInvocationID.x; + + if (index >= arrayLength(&particlesA.particles)) { + return; + } + + var vPos = particlesA.particles[index].pos; + var vVel = particlesA.particles[index].vel; + var cMass = vec2(0.0, 0.0); + var cVel = vec2(0.0, 0.0); + var colVel = vec2(0.0, 0.0); + var cMassCount : u32 = 0u; + var cVelCount : u32 = 0u; + var pos : vec2; + var vel : vec2; + + for (var i : u32 = 0u; i < arrayLength(&particlesA.particles); i = i + 1u) { + if (i == index) { + continue; + } + + pos = particlesA.particles[i].pos.xy; + vel = particlesA.particles[i].vel.xy; + if (distance(pos, vPos) < params.rule1Distance) { + cMass = cMass + pos; + cMassCount = cMassCount + 1u; + } + if (distance(pos, vPos) < params.rule2Distance) { + colVel = colVel - (pos - vPos); + } + if (distance(pos, vPos) < params.rule3Distance) { + cVel = cVel + vel; + cVelCount = cVelCount + 1u; + } + } + if (cMassCount > 0u) { + var temp = f32(cMassCount); + cMass = (cMass / vec2(temp, temp)) - vPos; + } + if (cVelCount > 0u) { + var temp = f32(cVelCount); + cVel = cVel / vec2(temp, temp); + } + vVel = vVel + (cMass * params.rule1Scale) + (colVel * params.rule2Scale) + + (cVel * params.rule3Scale); + + // clamp velocity for a more pleasing simulation + vVel = normalize(vVel) * clamp(length(vVel), 0.0, 0.1); + // kinematic update + vPos = vPos + (vVel * params.deltaT); + // Wrap around boundary + if (vPos.x < -1.0) { + vPos.x = 1.0; + } + if (vPos.x > 1.0) { + vPos.x = -1.0; + } + if (vPos.y < -1.0) { + vPos.y = 1.0; + } + if (vPos.y > 1.0) { + vPos.y = -1.0; + } + // Write back + particlesB.particles[index].pos = vPos; + particlesB.particles[index].vel = vVel; +} diff --git a/src/core/examples/sysgpu/clear-color/main.zig b/src/core/examples/sysgpu/clear-color/main.zig new file mode 100644 index 00000000..84aae5ea --- /dev/null +++ b/src/core/examples/sysgpu/clear-color/main.zig @@ -0,0 +1,83 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const renderer = @import("renderer.zig"); + +pub const App = @This(); + +pub const mach_core_options = core.ComptimeOptions{ + .use_wgpu = false, + .use_sysgpu = true, +}; + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +title_timer: core.Timer, + +pub fn init(app: *App) !void { + try core.init(.{}); + app.* = .{ + .title_timer = try core.Timer.start(), + }; +} + +pub fn deinit(app: *App) void { + _ = app; + defer _ = gpa.deinit(); + defer core.deinit(); +} + +pub fn update(app: *App) !bool { + var iter = core.pollEvents(); + while (iter.next()) |event| { + switch (event) { + .key_press => |ev| { + if (ev.key == .space) return true; + }, + .close => return true, + else => {}, + } + } + + app.render(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Clear Color [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} + +fn render(app: *App) void { + _ = app; + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = gpu.Color{ .r = 0.0, .g = 0.0, .b = 1.0, .a = 1.0 }, + .load_op = .clear, + .store_op = .store, + }; + + const encoder = core.device.createCommandEncoder(null); + const render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + }); + + const pass = encoder.beginRenderPass(&render_pass_info); + pass.end(); + pass.release(); + + var command = encoder.finish(null); + encoder.release(); + + var queue = core.queue; + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); +} diff --git a/src/core/examples/sysgpu/clear-color/renderer.zig b/src/core/examples/sysgpu/clear-color/renderer.zig new file mode 100644 index 00000000..1ad7924a --- /dev/null +++ b/src/core/examples/sysgpu/clear-color/renderer.zig @@ -0,0 +1,32 @@ +const core = @import("mach").core; +const gpu = core.gpu; + +pub const Renderer = @This(); + +pub fn RenderUpdate() void { + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = gpu.Color{ .r = 0.0, .g = 0.0, .b = 1.0, .a = 1.0 }, + .load_op = .clear, + .store_op = .store, + }; + + const encoder = core.device.createCommandEncoder(null); + const render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + }); + + const pass = encoder.beginRenderPass(&render_pass_info); + pass.end(); + pass.release(); + + var command = encoder.finish(null); + encoder.release(); + + const queue = core.queue; + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); +} diff --git a/src/core/examples/sysgpu/cubemap/cube_mesh.zig b/src/core/examples/sysgpu/cubemap/cube_mesh.zig new file mode 100644 index 00000000..ae5b2912 --- /dev/null +++ b/src/core/examples/sysgpu/cubemap/cube_mesh.zig @@ -0,0 +1,49 @@ +pub const Vertex = extern struct { + pos: @Vector(4, f32), + col: @Vector(4, f32), + uv: @Vector(2, f32), +}; + +pub const vertices = [_]Vertex{ + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 1, 0 } }, + + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 0 } }, + + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } }, + + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } }, + + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } }, + + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } }, +}; diff --git a/src/core/examples/sysgpu/cubemap/main.zig b/src/core/examples/sysgpu/cubemap/main.zig new file mode 100644 index 00000000..603d641c --- /dev/null +++ b/src/core/examples/sysgpu/cubemap/main.zig @@ -0,0 +1,397 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const zm = @import("zmath"); +const zigimg = @import("zigimg"); +const Vertex = @import("cube_mesh.zig").Vertex; +const vertices = @import("cube_mesh.zig").vertices; +const assets = @import("assets"); + +const UniformBufferObject = struct { + mat: zm.Mat, +}; + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +title_timer: core.Timer, +timer: core.Timer, +pipeline: *gpu.RenderPipeline, +vertex_buffer: *gpu.Buffer, +uniform_buffer: *gpu.Buffer, +bind_group: *gpu.BindGroup, +depth_texture: *gpu.Texture, +depth_texture_view: *gpu.TextureView, + +pub const App = @This(); + +pub const mach_core_options = core.ComptimeOptions{ + .use_wgpu = false, + .use_sysgpu = true, +}; + +pub fn init(app: *App) !void { + try core.init(.{}); + + const allocator = gpa.allocator(); + + const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); + defer shader_module.release(); + + const vertex_attributes = [_]gpu.VertexAttribute{ + .{ .format = .float32x4, .offset = @offsetOf(Vertex, "pos"), .shader_location = 0 }, + .{ .format = .float32x2, .offset = @offsetOf(Vertex, "uv"), .shader_location = 1 }, + }; + + const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{ + .array_stride = @sizeOf(Vertex), + .step_mode = .vertex, + .attributes = &vertex_attributes, + }); + + const blend = gpu.BlendState{ + .color = .{ + .operation = .add, + .src_factor = .src_alpha, + .dst_factor = .one_minus_src_alpha, + }, + .alpha = .{ + .operation = .add, + .src_factor = .one, + .dst_factor = .zero, + }, + }; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + // Enable depth testing so that the fragment closest to the camera + // is rendered in front. + .depth_stencil = &.{ + .format = .depth24_plus, + .depth_write_enabled = .true, + .depth_compare = .less, + }, + .vertex = gpu.VertexState.init(.{ + .module = shader_module, + .entry_point = "vertex_main", + .buffers = &.{vertex_buffer_layout}, + }), + .primitive = .{ + // Since the cube has its face pointing outwards, cull_mode must be + // set to .front or .none here since we are inside the cube looking out. + // Ideally you would set this to .back and have a custom cube primitive + // with the faces pointing towards the inside of the cube. + .cull_mode = .none, + }, + }; + const pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + + const vertex_buffer = core.device.createBuffer(&.{ + .usage = .{ .vertex = true }, + .size = @sizeOf(Vertex) * vertices.len, + .mapped_at_creation = .true, + }); + const vertex_mapped = vertex_buffer.getMappedRange(Vertex, 0, vertices.len); + @memcpy(vertex_mapped.?, vertices[0..]); + vertex_buffer.unmap(); + + const uniform_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = @sizeOf(UniformBufferObject), + .mapped_at_creation = .false, + }); + + // Create a sampler with linear filtering for smooth interpolation. + const sampler = core.device.createSampler(&.{ + .mag_filter = .linear, + .min_filter = .linear, + }); + + const queue = core.queue; + + // WebGPU expects the cubemap textures in this order: (+X,-X,+Y,-Y,+Z,-Z) + var images: [6]zigimg.Image = undefined; + images[0] = try zigimg.Image.fromMemory(allocator, assets.skybox_posx_png); + defer images[0].deinit(); + images[1] = try zigimg.Image.fromMemory(allocator, assets.skybox_negx_png); + defer images[1].deinit(); + images[2] = try zigimg.Image.fromMemory(allocator, assets.skybox_posy_png); + defer images[2].deinit(); + images[3] = try zigimg.Image.fromMemory(allocator, assets.skybox_negy_png); + defer images[3].deinit(); + images[4] = try zigimg.Image.fromMemory(allocator, assets.skybox_posz_png); + defer images[4].deinit(); + images[5] = try zigimg.Image.fromMemory(allocator, assets.skybox_negz_png); + defer images[5].deinit(); + + // Use the first image of the set for sizing + const img_size = gpu.Extent3D{ + .width = @as(u32, @intCast(images[0].width)), + .height = @as(u32, @intCast(images[0].height)), + }; + + // We set depth_or_array_layers to 6 here to indicate there are 6 images in this texture + const tex_size = gpu.Extent3D{ + .width = @as(u32, @intCast(images[0].width)), + .height = @as(u32, @intCast(images[0].height)), + .depth_or_array_layers = 6, + }; + + // Same as a regular texture, but with a Z of 6 (defined in tex_size) + const cube_texture = core.device.createTexture(&.{ + .size = tex_size, + .format = .rgba8_unorm, + .dimension = .dimension_2d, + .usage = .{ + .texture_binding = true, + .copy_dst = true, + .render_attachment = false, + }, + }); + + const data_layout = gpu.Texture.DataLayout{ + .bytes_per_row = @as(u32, @intCast(images[0].width * 4)), + .rows_per_image = @as(u32, @intCast(images[0].height)), + }; + + const encoder = core.device.createCommandEncoder(null); + + // We have to create a staging buffer, copy all the image data into the + // staging buffer at the correct Z offset, encode a command to copy + // the buffer to the texture for each image, then push it to the command + // queue + var staging_buff: [6]*gpu.Buffer = undefined; + var i: u32 = 0; + while (i < 6) : (i += 1) { + staging_buff[i] = core.device.createBuffer(&.{ + .usage = .{ .copy_src = true, .map_write = true }, + .size = @as(u64, @intCast(images[0].width)) * @as(u64, @intCast(images[0].height)) * @sizeOf(u32), + .mapped_at_creation = .true, + }); + switch (images[i].pixels) { + .rgba32 => |pixels| { + // Map a section of the staging buffer + const staging_map = staging_buff[i].getMappedRange(u32, 0, @as(u64, @intCast(images[i].width)) * @as(u64, @intCast(images[i].height))); + // Copy the image data into the mapped buffer + @memcpy(staging_map.?, @as([]u32, @ptrCast(@alignCast(pixels)))); + // And release the mapping + staging_buff[i].unmap(); + }, + .rgb24 => |pixels| { + const staging_map = staging_buff[i].getMappedRange(u32, 0, @as(u64, @intCast(images[i].width)) * @as(u64, @intCast(images[i].height))); + // In this case, we have to convert the data to rgba32 first + const data = try rgb24ToRgba32(allocator, pixels); + defer data.deinit(allocator); + @memcpy(staging_map.?, @as([]u32, @ptrCast(@alignCast(data.rgba32)))); + staging_buff[i].unmap(); + }, + else => @panic("unsupported image color format"), + } + + // These define the source and target for the buffer to texture copy command + const copy_buff = gpu.ImageCopyBuffer{ + .layout = data_layout, + .buffer = staging_buff[i], + }; + const copy_tex = gpu.ImageCopyTexture{ + .texture = cube_texture, + .origin = gpu.Origin3D{ .x = 0, .y = 0, .z = i }, + }; + + // Encode the copy command, we do this for every image in the texture. + encoder.copyBufferToTexture(©_buff, ©_tex, &img_size); + staging_buff[i].release(); + } + // Now that the commands to copy our buffer data to the texture is filled, + // push the encoded commands over to the queue and execute to get the + // texture filled with the image data. + var command = encoder.finish(null); + encoder.release(); + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + + // The textureView in the bind group needs dimension defined as "dimension_cube". + const cube_texture_view = cube_texture.createView(&gpu.TextureView.Descriptor{ .dimension = .dimension_cube }); + cube_texture.release(); + + const bind_group_layout = pipeline.getBindGroupLayout(0); + const bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bind_group_layout, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, uniform_buffer, 0, @sizeOf(UniformBufferObject), @sizeOf(UniformBufferObject)), + gpu.BindGroup.Entry.sampler(1, sampler), + gpu.BindGroup.Entry.textureView(2, cube_texture_view), + }, + }), + ); + sampler.release(); + cube_texture_view.release(); + bind_group_layout.release(); + + const depth_texture = core.device.createTexture(&gpu.Texture.Descriptor{ + .size = gpu.Extent3D{ + .width = core.descriptor.width, + .height = core.descriptor.height, + }, + .format = .depth24_plus, + .usage = .{ + .render_attachment = true, + .texture_binding = true, + }, + }); + + const depth_texture_view = depth_texture.createView(&gpu.TextureView.Descriptor{ + .format = .depth24_plus, + .dimension = .dimension_2d, + .array_layer_count = 1, + .mip_level_count = 1, + }); + + app.timer = try core.Timer.start(); + app.title_timer = try core.Timer.start(); + app.pipeline = pipeline; + app.vertex_buffer = vertex_buffer; + app.uniform_buffer = uniform_buffer; + app.bind_group = bind_group; + app.depth_texture = depth_texture; + app.depth_texture_view = depth_texture_view; +} + +pub fn deinit(app: *App) void { + defer _ = gpa.deinit(); + defer core.deinit(); + + app.pipeline.release(); + app.vertex_buffer.release(); + app.uniform_buffer.release(); + app.bind_group.release(); + app.depth_texture.release(); + app.depth_texture_view.release(); +} + +pub fn update(app: *App) !bool { + var iter = core.pollEvents(); + while (iter.next()) |event| { + switch (event) { + .key_press => |ev| { + if (ev.key == .space) return true; + }, + .close => return true, + .framebuffer_resize => |ev| { + // If window is resized, recreate depth buffer otherwise we cannot use it. + app.depth_texture.release(); + app.depth_texture = core.device.createTexture(&gpu.Texture.Descriptor{ + .size = gpu.Extent3D{ + .width = ev.width, + .height = ev.height, + }, + .format = .depth24_plus, + .usage = .{ + .render_attachment = true, + .texture_binding = true, + }, + }); + app.depth_texture_view.release(); + app.depth_texture_view = app.depth_texture.createView(&gpu.TextureView.Descriptor{ + .format = .depth24_plus, + .dimension = .dimension_2d, + .array_layer_count = 1, + .mip_level_count = 1, + }); + }, + else => {}, + } + } + + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = .{ .r = 0.5, .g = 0.5, .b = 0.5, .a = 0.0 }, + .load_op = .clear, + .store_op = .store, + }; + + const encoder = core.device.createCommandEncoder(null); + const render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + .depth_stencil_attachment = &.{ + .view = app.depth_texture_view, + .depth_clear_value = 1.0, + .depth_load_op = .clear, + .depth_store_op = .store, + }, + }); + + { + const time = app.timer.read(); + const aspect = @as(f32, @floatFromInt(core.descriptor.width)) / @as(f32, @floatFromInt(core.descriptor.height)); + const proj = zm.perspectiveFovRh((2 * std.math.pi) / 5.0, aspect, 0.1, 3000); + const model = zm.mul( + zm.scaling(1000, 1000, 1000), + zm.rotationX(std.math.pi / 2.0 * 3.0), + ); + const view = zm.mul( + zm.mul( + zm.lookAtRh( + zm.Vec{ 0, 0, 0, 1 }, + zm.Vec{ 1, 0, 0, 1 }, + zm.Vec{ 0, 0, 1, 0 }, + ), + zm.rotationY(time * 0.2), + ), + zm.rotationX((std.math.pi / 10.0) * std.math.sin(time)), + ); + + const mvp = zm.mul(zm.mul(zm.transpose(model), view), proj); + const ubo = UniformBufferObject{ .mat = mvp }; + + encoder.writeBuffer(app.uniform_buffer, 0, &[_]UniformBufferObject{ubo}); + } + + const pass = encoder.beginRenderPass(&render_pass_info); + pass.setPipeline(app.pipeline); + pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len); + pass.setBindGroup(0, app.bind_group, &.{}); + pass.draw(vertices.len, 1, 0, 0); + pass.end(); + pass.release(); + + var command = encoder.finish(null); + encoder.release(); + + const queue = core.queue; + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Cube Map [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} + +fn rgb24ToRgba32(allocator: std.mem.Allocator, in: []zigimg.color.Rgb24) !zigimg.color.PixelStorage { + const out = try zigimg.color.PixelStorage.init(allocator, .rgba32, in.len); + var i: usize = 0; + while (i < in.len) : (i += 1) { + out.rgba32[i] = zigimg.color.Rgba32{ .r = in[i].r, .g = in[i].g, .b = in[i].b, .a = 255 }; + } + return out; +} diff --git a/src/core/examples/sysgpu/cubemap/shader.wgsl b/src/core/examples/sysgpu/cubemap/shader.wgsl new file mode 100644 index 00000000..1442990c --- /dev/null +++ b/src/core/examples/sysgpu/cubemap/shader.wgsl @@ -0,0 +1,34 @@ +struct Uniforms { + modelViewProjectionMatrix : mat4x4, +} +@binding(0) @group(0) var uniforms : Uniforms; + +struct VertexOutput { + @builtin(position) Position : vec4, + @location(0) fragUV : vec2, + @location(1) fragPosition: vec4, +} + +@vertex +fn vertex_main( + @location(0) position : vec4, + @location(1) uv : vec2 +) -> VertexOutput { + var output : VertexOutput; + output.Position = uniforms.modelViewProjectionMatrix * position; + output.fragUV = uv; + output.fragPosition = 0.5 * (position + vec4(1.0, 1.0, 1.0, 1.0)); + return output; +} + +@group(0) @binding(1) var mySampler: sampler; +@group(0) @binding(2) var myTexture: texture_cube; + +@fragment +fn frag_main( + @location(0) fragUV: vec2, + @location(1) fragPosition: vec4 +) -> @location(0) vec4 { + var cubemapVec = fragPosition.xyz - vec3(0.5, 0.5, 0.5); + return textureSample(myTexture, mySampler, cubemapVec); +} diff --git a/src/core/examples/sysgpu/deferred-rendering/fragmentDeferredRendering.wgsl b/src/core/examples/sysgpu/deferred-rendering/fragmentDeferredRendering.wgsl new file mode 100644 index 00000000..51d7a93c --- /dev/null +++ b/src/core/examples/sysgpu/deferred-rendering/fragmentDeferredRendering.wgsl @@ -0,0 +1,89 @@ + +@group(0) @binding(0) var gBufferNormal: texture_2d; +@group(0) @binding(1) var gBufferAlbedo: texture_2d; +@group(0) @binding(2) var gBufferDepth: texture_depth_2d; + +struct LightData { + position : vec4, + // TODO - vec3 alignment + color_x : f32, + color_y : f32, + color_z : f32, + radius : f32, +} +struct LightsBuffer { + lights: array, +} +@group(1) @binding(0) var lightsBuffer: LightsBuffer; + +struct Config { + numLights : u32, +} +struct Camera { + viewProjectionMatrix : mat4x4, + invViewProjectionMatrix : mat4x4, +} +@group(1) @binding(1) var config: Config; +@group(1) @binding(2) var camera: Camera; + +fn world_from_screen_coord(coord : vec2, depth_sample: f32) -> vec3 { + // reconstruct world-space position from the screen coordinate. + let posClip = vec4(coord.x * 2.0 - 1.0, (1.0 - coord.y) * 2.0 - 1.0, depth_sample, 1.0); + let posWorldW = camera.invViewProjectionMatrix * posClip; + let posWorld = posWorldW.xyz / posWorldW.www; + return posWorld; +} + +@fragment +fn main( + @builtin(position) coord : vec4 +) -> @location(0) vec4 { + // TODO - variable initialization + var result = vec3(0, 0, 0); + + let depth = textureLoad( + gBufferDepth, + vec2(floor(coord.xy)), + 0 + ); + + // Don't light the sky. + if (depth >= 1.0) { + discard; + } + + let bufferSize = textureDimensions(gBufferDepth); + let coordUV = coord.xy / vec2(bufferSize); + let position = world_from_screen_coord(coordUV, depth); + + let normal = textureLoad( + gBufferNormal, + vec2(floor(coord.xy)), + 0 + ).xyz; + + let albedo = textureLoad( + gBufferAlbedo, + vec2(floor(coord.xy)), + 0 + ).rgb; + + for (var i = 0u; i < config.numLights; i++) { + // TODO - vec3 alignment + let lightColor = vec3(lightsBuffer.lights[i].color_x, lightsBuffer.lights[i].color_y, lightsBuffer.lights[i].color_z); + let L = lightsBuffer.lights[i].position.xyz - position; + let distance = length(L); + if (distance > lightsBuffer.lights[i].radius) { + continue; + } + let lambert = max(dot(normal, normalize(L)), 0.0); + result += vec3( + lambert * pow(1.0 - distance / lightsBuffer.lights[i].radius, 2.0) * lightColor * albedo + ); + } + + // some manual ambient + result += vec3(0.2); + + return vec4(result, 1.0); +} diff --git a/src/core/examples/sysgpu/deferred-rendering/fragmentGBuffersDebugView.wgsl b/src/core/examples/sysgpu/deferred-rendering/fragmentGBuffersDebugView.wgsl new file mode 100644 index 00000000..cedf7645 --- /dev/null +++ b/src/core/examples/sysgpu/deferred-rendering/fragmentGBuffersDebugView.wgsl @@ -0,0 +1,44 @@ + +@group(0) @binding(0) var gBufferNormal: texture_2d; +@group(0) @binding(1) var gBufferAlbedo: texture_2d; +@group(0) @binding(2) var gBufferDepth: texture_depth_2d; + +@group(1) @binding(0) var canvas : CanvasConstants; + +struct CanvasConstants { + size: vec2, +} + +@fragment +fn main( + @builtin(position) coord : vec4 +) -> @location(0) vec4 { + var result : vec4; + let c = coord.xy / vec2(canvas.size.x, canvas.size.y); + if (c.x < 0.33333) { + let rawDepth = textureLoad( + gBufferDepth, + vec2(floor(coord.xy)), + 0 + ); + // remap depth into something a bit more visible + let depth = (1.0 - rawDepth) * 50.0; + result = vec4(depth); + } else if (c.x < 0.66667) { + result = textureLoad( + gBufferNormal, + vec2(floor(coord.xy)), + 0 + ); + result.x = (result.x + 1.0) * 0.5; + result.y = (result.y + 1.0) * 0.5; + result.z = (result.z + 1.0) * 0.5; + } else { + result = textureLoad( + gBufferAlbedo, + vec2(floor(coord.xy)), + 0 + ); + } + return result; +} diff --git a/src/core/examples/sysgpu/deferred-rendering/fragmentWriteGBuffers.wgsl b/src/core/examples/sysgpu/deferred-rendering/fragmentWriteGBuffers.wgsl new file mode 100644 index 00000000..3cd2f652 --- /dev/null +++ b/src/core/examples/sysgpu/deferred-rendering/fragmentWriteGBuffers.wgsl @@ -0,0 +1,22 @@ +struct GBufferOutput { + @location(0) normal : vec4, + + // Textures: diffuse color, specular color, smoothness, emissive etc. could go here + @location(1) albedo : vec4, +} + +@fragment +fn main( + @location(0) fragNormal: vec3, + @location(1) fragUV : vec2 +) -> GBufferOutput { + // faking some kind of checkerboard texture + let uv = floor(30.0 * fragUV); + let c = 0.2 + 0.5 * ((uv.x + uv.y) - 2.0 * floor((uv.x + uv.y) / 2.0)); + + var output : GBufferOutput; + output.normal = vec4(fragNormal, 1.0); + output.albedo = vec4(c, c, c, 1.0); + + return output; +} diff --git a/src/core/examples/sysgpu/deferred-rendering/lightUpdate.wgsl b/src/core/examples/sysgpu/deferred-rendering/lightUpdate.wgsl new file mode 100644 index 00000000..548d8f8b --- /dev/null +++ b/src/core/examples/sysgpu/deferred-rendering/lightUpdate.wgsl @@ -0,0 +1,37 @@ +struct LightData { + position : vec4, + // TODO - vec3 alignment + color_x : f32, + color_y : f32, + color_z : f32, + radius : f32, +} +struct LightsBuffer { + lights: array, +} +@group(0) @binding(0) var lightsBuffer: LightsBuffer; + +struct Config { + numLights : u32, +} +@group(0) @binding(1) var config: Config; + +struct LightExtent { + min : vec4, + max : vec4, +} +@group(0) @binding(2) var lightExtent: LightExtent; + +@compute @workgroup_size(64, 1, 1) +fn main(@builtin(global_invocation_id) GlobalInvocationID : vec3) { + var index = GlobalInvocationID.x; + if (index >= config.numLights) { + return; + } + + lightsBuffer.lights[index].position.y = lightsBuffer.lights[index].position.y - 0.5 - 0.003 * (f32(index) - 64.0 * floor(f32(index) / 64.0)); + + if (lightsBuffer.lights[index].position.y < lightExtent.min.y) { + lightsBuffer.lights[index].position.y = lightExtent.max.y; + } +} diff --git a/src/core/examples/sysgpu/deferred-rendering/main.zig b/src/core/examples/sysgpu/deferred-rendering/main.zig new file mode 100644 index 00000000..a793c0c0 --- /dev/null +++ b/src/core/examples/sysgpu/deferred-rendering/main.zig @@ -0,0 +1,1224 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const m3d = @import("model3d"); +const zm = @import("zmath"); +const assets = @import("assets"); +const VertexWriter = @import("vertex_writer.zig").VertexWriter; + +pub const App = @This(); + +pub const mach_core_options = core.ComptimeOptions{ + .use_wgpu = false, + .use_sysgpu = true, +}; + +const Vec2 = [2]f32; +const Vec3 = [3]f32; +const Vec4 = [4]f32; +const Mat4 = [4]Vec4; + +fn Dimensions2D(comptime T: type) type { + return struct { + width: T, + height: T, + }; +} + +const Vertex = extern struct { + position: Vec3, + normal: Vec3, + uv: Vec2, +}; + +const ViewMatrices = struct { + up_vector: zm.Vec, + origin: zm.Vec, + projection_matrix: zm.Mat, + view_proj_matrix: zm.Mat, +}; + +const TextureQuadPass = struct { + color_attachment: gpu.RenderPassColorAttachment, + descriptor: gpu.RenderPassDescriptor, +}; + +const WriteGBufferPass = struct { + color_attachments: [2]gpu.RenderPassColorAttachment, + depth_stencil_attachment: gpu.RenderPassDepthStencilAttachment, + descriptor: gpu.RenderPassDescriptor, +}; + +const RenderMode = enum(u32) { + rendering, + gbuffer_view, +}; + +const Settings = struct { + render_mode: RenderMode, + lights_count: i32, +}; + +// +// Constants +// + +const max_num_lights = 1024; +const light_data_stride = 8; +const light_extent_min = Vec3{ -50.0, -30.0, -50.0 }; +const light_extent_max = Vec3{ 50.0, 30.0, 50.0 }; +const camera_uniform_buffer_size = @sizeOf(Mat4) * 2; + +// +// Member variables +// + +const GBuffer = struct { + texture_2d_float16: *gpu.Texture, + texture_albedo: *gpu.Texture, + texture_depth: *gpu.Texture, + texture_views: [3]*gpu.TextureView, +}; + +const Lights = struct { + buffer: *gpu.Buffer, + buffer_size: u64, + extent_buffer: *gpu.Buffer, + extent_buffer_size: u64, + config_uniform_buffer: *gpu.Buffer, + config_uniform_buffer_size: u64, + buffer_bind_group: *gpu.BindGroup, + buffer_bind_group_layout: *gpu.BindGroupLayout, + buffer_compute_bind_group: *gpu.BindGroup, + buffer_compute_bind_group_layout: *gpu.BindGroupLayout, +}; + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +title_timer: core.Timer, +timer: core.Timer, +delta_time: f32, + +camera_rotation: f32, +vertex_buffer: *gpu.Buffer, +vertex_count: u32, +index_buffer: *gpu.Buffer, +index_count: u32, +gbuffer: GBuffer, +model_uniform_buffer: *gpu.Buffer, +camera_uniform_buffer: *gpu.Buffer, +surface_size_uniform_buffer: *gpu.Buffer, +lights: Lights, +view_matrices: ViewMatrices, + +// Bind groups +scene_uniform_bind_group: *gpu.BindGroup, +surface_size_uniform_bind_group: *gpu.BindGroup, +gbuffer_textures_bind_group: *gpu.BindGroup, + +// Bind group layouts +scene_uniform_bind_group_layout: *gpu.BindGroupLayout, +surface_size_uniform_bind_group_layout: *gpu.BindGroupLayout, +gbuffer_textures_bind_group_layout: *gpu.BindGroupLayout, + +// Pipelines +write_gbuffers_pipeline: *gpu.RenderPipeline, +gbuffers_debug_view_pipeline: *gpu.RenderPipeline, +deferred_render_pipeline: *gpu.RenderPipeline, +light_update_compute_pipeline: *gpu.ComputePipeline, + +// Pipeline layouts +write_gbuffers_pipeline_layout: *gpu.PipelineLayout, +gbuffers_debug_view_pipeline_layout: *gpu.PipelineLayout, +deferred_render_pipeline_layout: *gpu.PipelineLayout, +light_update_compute_pipeline_layout: *gpu.PipelineLayout, + +// Render pass descriptor +write_gbuffer_pass: WriteGBufferPass, +texture_quad_pass: TextureQuadPass, +settings: Settings, + +screen_dimensions: Dimensions2D(u32), +is_paused: bool, + +// +// Functions +// + +pub fn init(app: *App) !void { + try core.init(.{}); + + // This example has some frame-rate-dependent animation, so restrict frame rate to 60hz. + core.setFrameRateLimit(60); + + app.timer = try core.Timer.start(); + app.title_timer = try core.Timer.start(); + + app.camera_rotation = 0.0; + app.is_paused = false; + app.settings.render_mode = .rendering; + app.settings.lights_count = 128; + + app.screen_dimensions = Dimensions2D(u32){ + .width = core.descriptor.width, + .height = core.descriptor.height, + }; + + try app.loadMeshFromModel3d(std.heap.c_allocator, assets.stanford_dragon_m3d); + app.prepareGBufferTextureRenderTargets(); + app.prepareBindGroupLayouts(); + app.prepareRenderPipelineLayouts(); + app.prepareWriteGBuffersPipeline(); + app.prepareGBuffersDebugViewPipeline(); + app.prepareDeferredRenderPipeline(); + app.setupRenderPasses(); + app.prepareUniformBuffers(); + app.prepareComputePipelineLayout(); + app.prepareLightUpdateComputePipeline(); + app.prepareLights(); + app.prepareViewMatrices(); + app.printControls(); +} + +pub fn deinit(app: *App) void { + defer _ = gpa.deinit(); + defer core.deinit(); + + app.write_gbuffers_pipeline.release(); + app.gbuffers_debug_view_pipeline.release(); + app.deferred_render_pipeline.release(); + app.light_update_compute_pipeline.release(); + + app.write_gbuffers_pipeline_layout.release(); + app.gbuffers_debug_view_pipeline_layout.release(); + app.deferred_render_pipeline_layout.release(); + app.light_update_compute_pipeline_layout.release(); + + app.scene_uniform_bind_group.release(); + app.surface_size_uniform_bind_group.release(); + app.gbuffer_textures_bind_group.release(); + + app.lights.buffer.release(); + app.lights.extent_buffer.release(); + app.lights.config_uniform_buffer.release(); + app.lights.buffer_bind_group.release(); + app.lights.buffer_bind_group_layout.release(); + app.lights.buffer_compute_bind_group.release(); + app.lights.buffer_compute_bind_group_layout.release(); + + app.gbuffer.texture_views[0].release(); + app.gbuffer.texture_views[1].release(); + app.gbuffer.texture_views[2].release(); + + app.gbuffer.texture_2d_float16.release(); + app.gbuffer.texture_albedo.release(); + app.gbuffer.texture_depth.release(); + + app.scene_uniform_bind_group_layout.release(); + app.surface_size_uniform_bind_group_layout.release(); + app.gbuffer_textures_bind_group_layout.release(); + + app.surface_size_uniform_buffer.release(); + app.model_uniform_buffer.release(); + app.camera_uniform_buffer.release(); + app.vertex_buffer.release(); + app.index_buffer.release(); +} + +pub fn update(app: *App) !bool { + app.delta_time = app.timer.lap(); + + var iter = core.pollEvents(); + while (iter.next()) |event| { + app.updateUI(event); + switch (event) { + .framebuffer_resize => |ev| { + app.screen_dimensions.width = ev.width; + app.screen_dimensions.height = ev.height; + + // TODO: we use destroy() here instead of release() because our reference counting + // is wrong somewhere else. + app.gbuffer.texture_2d_float16.release(); + app.gbuffer.texture_albedo.release(); + app.gbuffer.texture_depth.release(); + app.gbuffer.texture_views[0].release(); + app.gbuffer.texture_views[1].release(); + app.gbuffer.texture_views[2].release(); + app.gbuffer_textures_bind_group.release(); + + app.prepareGBufferTextureRenderTargets(); + app.setupRenderPasses(); + + const bind_group_entries = [_]gpu.BindGroup.Entry{ + gpu.BindGroup.Entry.textureView(0, app.gbuffer.texture_views[0]), + gpu.BindGroup.Entry.textureView(1, app.gbuffer.texture_views[1]), + gpu.BindGroup.Entry.textureView(2, app.gbuffer.texture_views[2]), + }; + app.gbuffer_textures_bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = app.gbuffer_textures_bind_group_layout, + .entries = &bind_group_entries, + }), + ); + + app.prepareViewMatrices(); + }, + .close => return true, + else => {}, + } + } + + if (!app.is_paused) { + app.updateUniformBuffers(); + } + + const command = try app.buildCommandBuffer(); + const queue = core.queue; + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + core.swap_chain.getCurrentTextureView().?.release(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Deferred Rendering [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} + +fn loadMeshFromModel3d(app: *App, allocator: std.mem.Allocator, model_data: [:0]const u8) !void { + const m3d_model = m3d.load(model_data, null, null, null) orelse return error.LoadModelFailed; + + const vertex_count = m3d_model.handle.numvertex; + const vertices = m3d_model.handle.vertex[0..vertex_count]; + + const face_count = m3d_model.handle.numface; + app.index_count = (face_count * 3) + 6; + + var vertex_writer = try VertexWriter(Vertex, u16).init( + allocator, + @as(u16, @intCast(app.index_count)), + @as(u16, @intCast(vertex_count)), + @as(u16, @intCast(face_count * 3)), + ); + defer vertex_writer.deinit(allocator); + + const scale: f32 = 80.0; + const plane_xy = [2]usize{ 0, 1 }; + var extent_min = [2]f32{ std.math.floatMax(f32), std.math.floatMax(f32) }; + var extent_max = [2]f32{ std.math.floatMin(f32), std.math.floatMin(f32) }; + + var i: usize = 0; + while (i < face_count) : (i += 1) { + const face = m3d_model.handle.face[i]; + var x: usize = 0; + while (x < 3) : (x += 1) { + const vertex_index = face.vertex[x]; + const normal_index = face.normal[x]; + const position = Vec3{ + vertices[vertex_index].x * scale, + vertices[vertex_index].y * scale, + vertices[vertex_index].z * scale, + }; + extent_min[0] = @min(position[plane_xy[0]], extent_min[0]); + extent_min[1] = @min(position[plane_xy[1]], extent_min[1]); + extent_max[0] = @max(position[plane_xy[0]], extent_max[0]); + extent_max[1] = @max(position[plane_xy[1]], extent_max[1]); + const vertex = Vertex{ .position = position, .normal = .{ + vertices[normal_index].x, + vertices[normal_index].y, + vertices[normal_index].z, + }, .uv = .{ position[plane_xy[0]], position[plane_xy[1]] } }; + vertex_writer.put(vertex, @as(u16, @intCast(vertex_index))); + } + } + + const vertex_buffer = vertex_writer.vertices[0 .. vertex_writer.next_packed_index + 4]; + const index_buffer = vertex_writer.indices; + + app.vertex_count = @as(u32, @intCast(vertex_buffer.len)); + + // + // Compute UV values + // + for (vertex_buffer) |*vertex| { + vertex.uv = .{ + (vertex.uv[0] - extent_min[0]) / (extent_max[0] - extent_min[0]), + (vertex.uv[1] - extent_min[1]) / (extent_max[1] - extent_min[1]), + }; + } + + // + // Manually append ground plane to mesh + // + { + const last_vertex_index: u16 = @as(u16, @intCast(vertex_buffer.len - 4)); + const index_base = index_buffer.len - 6; + index_buffer[index_base + 0] = last_vertex_index; + index_buffer[index_base + 1] = last_vertex_index + 2; + index_buffer[index_base + 2] = last_vertex_index + 1; + index_buffer[index_base + 3] = last_vertex_index; + index_buffer[index_base + 4] = last_vertex_index + 1; + index_buffer[index_base + 5] = last_vertex_index + 3; + } + + { + const index_base = vertex_buffer.len - 4; + vertex_buffer[index_base + 0].position = .{ -100.0, 20.0, -100.0 }; + vertex_buffer[index_base + 1].position = .{ 100.0, 20.0, 100.0 }; + vertex_buffer[index_base + 2].position = .{ -100.0, 20.0, 100.0 }; + vertex_buffer[index_base + 3].position = .{ 100.0, 20.0, -100.0 }; + vertex_buffer[index_base + 0].normal = .{ 0.0, 1.0, 0.0 }; + vertex_buffer[index_base + 1].normal = .{ 0.0, 1.0, 0.0 }; + vertex_buffer[index_base + 2].normal = .{ 0.0, 1.0, 0.0 }; + vertex_buffer[index_base + 3].normal = .{ 0.0, 1.0, 0.0 }; + vertex_buffer[index_base + 0].uv = .{ 0.0, 0.0 }; + vertex_buffer[index_base + 1].uv = .{ 1.0, 1.0 }; + vertex_buffer[index_base + 2].uv = .{ 0.0, 1.0 }; + vertex_buffer[index_base + 3].uv = .{ 1.0, 0.0 }; + } + + { + const buffer_size = vertex_buffer.len * @sizeOf(Vertex); + app.vertex_buffer = core.device.createBuffer(&.{ + .usage = .{ .vertex = true }, + .size = roundToMultipleOf4(u64, buffer_size), + .mapped_at_creation = .true, + }); + var mapping = app.vertex_buffer.getMappedRange(Vertex, 0, vertex_buffer.len).?; + @memcpy(mapping[0..vertex_buffer.len], vertex_buffer); + app.vertex_buffer.unmap(); + } + { + const buffer_size = index_buffer.len * @sizeOf(u16); + app.index_buffer = core.device.createBuffer(&.{ + .usage = .{ .index = true }, + .size = roundToMultipleOf4(u64, buffer_size), + .mapped_at_creation = .true, + }); + var mapping = app.index_buffer.getMappedRange(u16, 0, index_buffer.len).?; + @memcpy(mapping[0..index_buffer.len], index_buffer); + app.index_buffer.unmap(); + } +} + +fn prepareGBufferTextureRenderTargets(app: *App) void { + var screen_extent = gpu.Extent3D{ + .width = app.screen_dimensions.width, + .height = app.screen_dimensions.height, + .depth_or_array_layers = 2, + }; + screen_extent.depth_or_array_layers = 1; + app.gbuffer.texture_2d_float16 = core.device.createTexture(&.{ + .size = screen_extent, + .format = .rgba16_float, + .mip_level_count = 1, + .sample_count = 1, + .usage = .{ + .texture_binding = true, + .render_attachment = true, + }, + }); + app.gbuffer.texture_albedo = core.device.createTexture(&.{ + .size = screen_extent, + .format = .bgra8_unorm, + .usage = .{ + .texture_binding = true, + .render_attachment = true, + }, + }); + app.gbuffer.texture_depth = core.device.createTexture(&.{ + .size = screen_extent, + .mip_level_count = 1, + .sample_count = 1, + .dimension = .dimension_2d, + .format = .depth24_plus, + .usage = .{ + .texture_binding = true, + .render_attachment = true, + }, + }); + + var texture_view_descriptor = gpu.TextureView.Descriptor{ + .format = .undefined, + .dimension = .dimension_2d, + .array_layer_count = 1, + .aspect = .all, + .base_array_layer = 0, + }; + + texture_view_descriptor.format = .rgba16_float; + app.gbuffer.texture_views[0] = app.gbuffer.texture_2d_float16.createView(&texture_view_descriptor); + + texture_view_descriptor.format = .bgra8_unorm; + app.gbuffer.texture_views[1] = app.gbuffer.texture_albedo.createView(&texture_view_descriptor); + + texture_view_descriptor.format = .depth24_plus; + app.gbuffer.texture_views[2] = app.gbuffer.texture_depth.createView(&texture_view_descriptor); +} + +fn prepareBindGroupLayouts(app: *App) void { + { + const bind_group_layout_entries = [_]gpu.BindGroupLayout.Entry{ + gpu.BindGroupLayout.Entry.texture(0, .{ .fragment = true }, .unfilterable_float, .dimension_2d, false), + gpu.BindGroupLayout.Entry.texture(1, .{ .fragment = true }, .unfilterable_float, .dimension_2d, false), + gpu.BindGroupLayout.Entry.texture(2, .{ .fragment = true }, .depth, .dimension_2d, false), + }; + app.gbuffer_textures_bind_group_layout = core.device.createBindGroupLayout( + &gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &bind_group_layout_entries, + }), + ); + } + { + const min_binding_size = light_data_stride * max_num_lights * @sizeOf(f32); + const visibility = gpu.ShaderStageFlags{ .fragment = true, .compute = true }; + const bind_group_layout_entries = [_]gpu.BindGroupLayout.Entry{ + gpu.BindGroupLayout.Entry.buffer( + 0, + visibility, + .read_only_storage, + false, + min_binding_size, + ), + gpu.BindGroupLayout.Entry.buffer(1, visibility, .uniform, false, @sizeOf(u32)), + gpu.BindGroupLayout.Entry.buffer(2, .{ .fragment = true }, .uniform, false, @sizeOf(Mat4) * 2), + }; + app.lights.buffer_bind_group_layout = core.device.createBindGroupLayout( + &gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &bind_group_layout_entries, + }), + ); + } + { + const bind_group_layout_entries = [_]gpu.BindGroupLayout.Entry{ + gpu.BindGroupLayout.Entry.buffer(0, .{ .fragment = true }, .uniform, false, @sizeOf(Vec2)), + }; + app.surface_size_uniform_bind_group_layout = core.device.createBindGroupLayout( + &gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &bind_group_layout_entries, + }), + ); + } + { + const bind_group_layout_entries = [_]gpu.BindGroupLayout.Entry{ + gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, false, @sizeOf(Mat4) * 2), + gpu.BindGroupLayout.Entry.buffer(1, .{ .vertex = true }, .uniform, false, @sizeOf(Mat4) * 2), + }; + app.scene_uniform_bind_group_layout = core.device.createBindGroupLayout( + &gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &bind_group_layout_entries, + }), + ); + } + { + const bind_group_layout_entries = [_]gpu.BindGroupLayout.Entry{ + gpu.BindGroupLayout.Entry.buffer(0, .{ .compute = true }, .storage, false, @sizeOf(f32) * light_data_stride * max_num_lights), + gpu.BindGroupLayout.Entry.buffer(1, .{ .compute = true }, .uniform, false, @sizeOf(u32)), + gpu.BindGroupLayout.Entry.buffer(2, .{ .compute = true }, .uniform, false, camera_uniform_buffer_size), + }; + app.lights.buffer_compute_bind_group_layout = core.device.createBindGroupLayout( + &gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &bind_group_layout_entries, + }), + ); + } +} + +fn prepareRenderPipelineLayouts(app: *App) void { + { + // Write GBuffers pipeline layout + const bind_group_layouts = [_]*gpu.BindGroupLayout{app.scene_uniform_bind_group_layout}; + app.write_gbuffers_pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &bind_group_layouts, + })); + } + { + // GBuffers debug view pipeline layout + const bind_group_layouts = [_]*gpu.BindGroupLayout{ + app.gbuffer_textures_bind_group_layout, + app.surface_size_uniform_bind_group_layout, + }; + app.gbuffers_debug_view_pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &bind_group_layouts, + })); + } + { + // Deferred render pipeline layout + const bind_group_layouts = [_]*gpu.BindGroupLayout{ + app.gbuffer_textures_bind_group_layout, + app.lights.buffer_bind_group_layout, + app.surface_size_uniform_bind_group_layout, + }; + app.deferred_render_pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &bind_group_layouts, + })); + } +} + +fn prepareWriteGBuffersPipeline(app: *App) void { + const color_target_states = [_]gpu.ColorTargetState{ + .{ .format = .rgba16_float }, + .{ .format = .bgra8_unorm }, + }; + + const write_gbuffers_vertex_buffer_layout = gpu.VertexBufferLayout.init(.{ + .array_stride = @sizeOf(Vertex), + .step_mode = .vertex, + .attributes = &.{ + .{ .format = .float32x3, .offset = @offsetOf(Vertex, "position"), .shader_location = 0 }, + .{ .format = .float32x3, .offset = @offsetOf(Vertex, "normal"), .shader_location = 1 }, + .{ .format = .float32x2, .offset = @offsetOf(Vertex, "uv"), .shader_location = 2 }, + }, + }); + + const vertex_shader_module = core.device.createShaderModuleWGSL( + "vertexWriteGBuffers.wgsl", + @embedFile("vertexWriteGBuffers.wgsl"), + ); + const fragment_shader_module = core.device.createShaderModuleWGSL( + "fragmentWriteGBuffers.wgsl", + @embedFile("fragmentWriteGBuffers.wgsl"), + ); + + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .label = "gbuffers_pipeline", + .layout = app.write_gbuffers_pipeline_layout, + .primitive = .{ .cull_mode = .back }, + .depth_stencil = &.{ + .format = .depth24_plus, + .depth_write_enabled = .true, + .depth_compare = .less, + }, + .vertex = gpu.VertexState.init(.{ + .module = vertex_shader_module, + .entry_point = "main", + .buffers = &.{write_gbuffers_vertex_buffer_layout}, + }), + .fragment = &gpu.FragmentState.init(.{ + .module = fragment_shader_module, + .entry_point = "main", + .targets = &color_target_states, + }), + }; + app.write_gbuffers_pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + + vertex_shader_module.release(); + fragment_shader_module.release(); +} + +fn prepareGBuffersDebugViewPipeline(app: *App) void { + const blend_component_descriptor = gpu.BlendComponent{ + .operation = .add, + .src_factor = .one, + .dst_factor = .zero, + }; + + const color_target_state = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &.{ + .color = blend_component_descriptor, + .alpha = blend_component_descriptor, + }, + }; + + const vertex_shader_module = core.device.createShaderModuleWGSL( + "vertexTextureQuad.wgsl", + @embedFile("vertexTextureQuad.wgsl"), + ); + const fragment_shader_module = core.device.createShaderModuleWGSL( + "fragmentGBuffersDebugView.wgsl", + @embedFile("fragmentGBuffersDebugView.wgsl"), + ); + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .layout = app.gbuffers_debug_view_pipeline_layout, + .primitive = .{ + .cull_mode = .back, + }, + .vertex = gpu.VertexState.init(.{ + .module = vertex_shader_module, + .entry_point = "main", + }), + .fragment = &gpu.FragmentState.init(.{ + .module = fragment_shader_module, + .entry_point = "main", + .targets = &.{color_target_state}, + }), + }; + app.gbuffers_debug_view_pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + vertex_shader_module.release(); + fragment_shader_module.release(); +} + +fn prepareDeferredRenderPipeline(app: *App) void { + const blend_component_descriptor = gpu.BlendComponent{ + .operation = .add, + .src_factor = .one, + .dst_factor = .zero, + }; + + const color_target_state = gpu.ColorTargetState{ + .format = .bgra8_unorm, + .blend = &.{ + .color = blend_component_descriptor, + .alpha = blend_component_descriptor, + }, + }; + + const vertex_shader_module = core.device.createShaderModuleWGSL( + "vertexTextureQuad.wgsl", + @embedFile("vertexTextureQuad.wgsl"), + ); + const fragment_shader_module = core.device.createShaderModuleWGSL( + "fragmentDeferredRendering.wgsl", + @embedFile("fragmentDeferredRendering.wgsl"), + ); + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .layout = app.deferred_render_pipeline_layout, + .primitive = .{ + .cull_mode = .back, + }, + .vertex = gpu.VertexState.init(.{ + .module = vertex_shader_module, + .entry_point = "main", + }), + .fragment = &gpu.FragmentState.init(.{ + .module = fragment_shader_module, + .entry_point = "main", + .targets = &.{color_target_state}, + }), + }; + app.deferred_render_pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + vertex_shader_module.release(); + fragment_shader_module.release(); +} + +fn setupRenderPasses(app: *App) void { + { + // Write GBuffer pass + app.write_gbuffer_pass.color_attachments = [_]gpu.RenderPassColorAttachment{ + .{ + .view = app.gbuffer.texture_views[0], + .load_op = .clear, + .store_op = .store, + .clear_value = .{ + .r = 0.0, + .g = 0.0, + .b = 1.0, + .a = 1.0, + }, + }, + .{ + .view = app.gbuffer.texture_views[1], + .load_op = .clear, + .store_op = .store, + .clear_value = .{ + .r = 0.0, + .g = 0.0, + .b = 0.0, + .a = 1.0, + }, + }, + }; + + app.write_gbuffer_pass.depth_stencil_attachment = gpu.RenderPassDepthStencilAttachment{ + .view = app.gbuffer.texture_views[2], + .depth_load_op = .clear, + .depth_store_op = .store, + .depth_clear_value = 1.0, + .stencil_clear_value = 1.0, + }; + + app.write_gbuffer_pass.descriptor = gpu.RenderPassDescriptor.init(.{ + .label = "write_gbuffer_pass", + .color_attachments = &app.write_gbuffer_pass.color_attachments, + .depth_stencil_attachment = &app.write_gbuffer_pass.depth_stencil_attachment, + }); + } + { + // Texture Quad Pass + app.texture_quad_pass.color_attachment = gpu.RenderPassColorAttachment{ + .clear_value = .{ + .r = 0.0, + .g = 0.0, + .b = 0.0, + .a = 1.0, + }, + .load_op = .clear, + .store_op = .store, + }; + + app.texture_quad_pass.descriptor = gpu.RenderPassDescriptor{ + .label = "texture_quad_pass(1)", + .color_attachment_count = 1, + .color_attachments = &[_]gpu.RenderPassColorAttachment{app.texture_quad_pass.color_attachment}, + }; + } +} + +fn prepareUniformBuffers(app: *App) void { + { + // Config uniform buffer + app.lights.config_uniform_buffer_size = @sizeOf(i32); + app.lights.config_uniform_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = app.lights.config_uniform_buffer_size, + .mapped_at_creation = .true, + }); + var config_data = app.lights.config_uniform_buffer.getMappedRange(i32, 0, 1).?; + config_data[0] = app.settings.lights_count; + app.lights.config_uniform_buffer.unmap(); + } + { + // Model uniform buffer + app.model_uniform_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = @sizeOf(Mat4) * 2, + }); + } + { + // Camera uniform buffer + app.camera_uniform_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = @sizeOf(Mat4) * 2, + }); + } + { + // Scene uniform bind group + const bind_group_entries = [_]gpu.BindGroup.Entry{ + .{ + .binding = 0, + .buffer = app.model_uniform_buffer, + .size = @sizeOf(Mat4) * 2, + .elem_size = @sizeOf(Mat4) * 2, + }, + .{ + .binding = 1, + .buffer = app.camera_uniform_buffer, + .size = camera_uniform_buffer_size, + .elem_size = camera_uniform_buffer_size, + }, + }; + const bind_group_layout = app.write_gbuffers_pipeline.getBindGroupLayout(0); + app.scene_uniform_bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .label = "scene_uniform_bind_group", + .layout = bind_group_layout, + .entries = &bind_group_entries, + }), + ); + bind_group_layout.release(); + } + { + // Surface size uniform buffer + app.surface_size_uniform_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = @sizeOf(f32) * 4, + }); + } + { + // Surface size uniform bind group + const bind_group_entries = [_]gpu.BindGroup.Entry{ + .{ + .binding = 0, + .buffer = app.surface_size_uniform_buffer, + .size = @sizeOf(f32) * 2, + .elem_size = @sizeOf(f32) * 2, + }, + }; + app.surface_size_uniform_bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = app.surface_size_uniform_bind_group_layout, + .entries = &bind_group_entries, + }), + ); + } + { + // GBuffer textures bind group + const bind_group_entries = [_]gpu.BindGroup.Entry{ + gpu.BindGroup.Entry.textureView(0, app.gbuffer.texture_views[0]), + gpu.BindGroup.Entry.textureView(1, app.gbuffer.texture_views[1]), + gpu.BindGroup.Entry.textureView(2, app.gbuffer.texture_views[2]), + }; + app.gbuffer_textures_bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = app.gbuffer_textures_bind_group_layout, + .entries = &bind_group_entries, + }), + ); + } +} + +fn prepareComputePipelineLayout(app: *App) void { + const bind_group_layouts = [_]*gpu.BindGroupLayout{app.lights.buffer_compute_bind_group_layout}; + app.light_update_compute_pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &bind_group_layouts, + })); +} + +fn prepareLightUpdateComputePipeline(app: *App) void { + const shader_module = core.device.createShaderModuleWGSL( + "lightUpdate.wgsl", + @embedFile("lightUpdate.wgsl"), + ); + app.light_update_compute_pipeline = core.device.createComputePipeline(&gpu.ComputePipeline.Descriptor{ + .compute = gpu.ProgrammableStageDescriptor{ + .module = shader_module, + .entry_point = "main", + }, + .layout = app.light_update_compute_pipeline_layout, + }); + shader_module.release(); +} + +fn prepareLights(app: *App) void { + // Lights data are uploaded in a storage buffer + // which could be updated/culled/etc. with a compute shader + const extent = comptime Vec3{ + light_extent_max[0] - light_extent_min[0], + light_extent_max[1] - light_extent_min[1], + light_extent_max[2] - light_extent_min[2], + }; + app.lights.buffer_size = @sizeOf(f32) * light_data_stride * max_num_lights; + app.lights.buffer = core.device.createBuffer(&.{ + .usage = .{ .storage = true }, + .size = app.lights.buffer_size, + .mapped_at_creation = .true, + }); + // We randomly populate lights randomly in a box range + // And simply move them along y-axis per frame to show they are dynamic lightings + var light_data = app.lights.buffer.getMappedRange(f32, 0, light_data_stride * max_num_lights).?; + + var xoroshiro = std.rand.Xoroshiro128.init(9273853284918); + const rng = std.rand.Random.init( + &xoroshiro, + std.rand.Xoroshiro128.fill, + ); + var i: usize = 0; + var offset: usize = 0; + while (i < max_num_lights) : (i += 1) { + offset = light_data_stride * i; + // Position + light_data[offset + 0] = rng.float(f32) * extent[0] + light_extent_min[0]; + light_data[offset + 1] = rng.float(f32) * extent[1] + light_extent_min[1]; + light_data[offset + 2] = rng.float(f32) * extent[2] + light_extent_min[2]; + light_data[offset + 3] = 1.0; + // Color + light_data[offset + 4] = rng.float(f32) * 2.0; + light_data[offset + 5] = rng.float(f32) * 2.0; + light_data[offset + 6] = rng.float(f32) * 2.0; + // Radius + light_data[offset + 7] = 20.0; + } + app.lights.buffer.unmap(); + // + // Lights extent buffer + // + app.lights.extent_buffer_size = @sizeOf(f32) * light_data_stride * max_num_lights; + app.lights.extent_buffer = core.device.createBuffer(&.{ + .usage = .{ .uniform = true, .copy_dst = true }, + .size = app.lights.extent_buffer_size, + }); + var light_extent_data = [1]f32{0.0} ** 8; + @memcpy(light_extent_data[0..3], &light_extent_min); + @memcpy(light_extent_data[4..7], &light_extent_max); + const queue = core.queue; + queue.writeBuffer( + app.lights.extent_buffer, + 0, + &light_extent_data, + ); + // + // Lights buffer bind group + // + { + const bind_group_entries = [_]gpu.BindGroup.Entry{ + .{ + .binding = 0, + .buffer = app.lights.buffer, + .size = app.lights.buffer_size, + .elem_size = @sizeOf(f32) * light_data_stride, + }, + .{ + .binding = 1, + .buffer = app.lights.config_uniform_buffer, + .size = app.lights.config_uniform_buffer_size, + .elem_size = @intCast(app.lights.config_uniform_buffer_size), + }, + .{ + .binding = 2, + .buffer = app.camera_uniform_buffer, + .size = camera_uniform_buffer_size, + .elem_size = camera_uniform_buffer_size, + }, + }; + app.lights.buffer_bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = app.lights.buffer_bind_group_layout, + .entries = &bind_group_entries, + }), + ); + } + // + // Lights buffer compute bind group + // + { + const bind_group_entries = [_]gpu.BindGroup.Entry{ + .{ + .binding = 0, + .buffer = app.lights.buffer, + .size = app.lights.buffer_size, + .elem_size = @sizeOf(f32) * light_data_stride, + }, + .{ + .binding = 1, + .buffer = app.lights.config_uniform_buffer, + .size = app.lights.config_uniform_buffer_size, + .elem_size = @intCast(app.lights.config_uniform_buffer_size), + }, + .{ + .binding = 2, + .buffer = app.lights.extent_buffer, + .size = app.lights.extent_buffer_size, + .elem_size = @intCast(app.lights.extent_buffer_size), + }, + }; + const bind_group_layout = app.light_update_compute_pipeline.getBindGroupLayout(0); + app.lights.buffer_compute_bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bind_group_layout, + .entries = &bind_group_entries, + }), + ); + bind_group_layout.release(); + } +} + +fn prepareViewMatrices(app: *App) void { + const screen_dimensions = Dimensions2D(f32){ + .width = @as(f32, @floatFromInt(app.screen_dimensions.width)), + .height = @as(f32, @floatFromInt(app.screen_dimensions.height)), + }; + const aspect: f32 = screen_dimensions.width / screen_dimensions.height; + const fov: f32 = 2.0 * std.math.pi / 5.0; + const znear: f32 = 1.0; + const zfar: f32 = 2000.0; + app.view_matrices.projection_matrix = zm.perspectiveFovRhGl(fov, aspect, znear, zfar); + const eye_position = zm.Vec{ 0.0, 50.0, -100.0, 0.0 }; + app.view_matrices.up_vector = zm.Vec{ 0.0, 1.0, 0.0, 0.0 }; + app.view_matrices.origin = zm.Vec{ 0.0, 0.0, 0.0, 0.0 }; + const view_matrix = zm.lookAtRh( + eye_position, + app.view_matrices.origin, + app.view_matrices.up_vector, + ); + const view_proj_matrix: zm.Mat = zm.mul(view_matrix, app.view_matrices.projection_matrix); + // Move the model so it's centered. + const model_matrix = zm.translationV(zm.Vec{ 0.0, -45.0, 0.0, 0.0 }); + const queue = core.queue; + queue.writeBuffer( + app.camera_uniform_buffer, + 0, + &view_proj_matrix, + ); + queue.writeBuffer( + app.model_uniform_buffer, + 0, + &model_matrix, + ); + const invert_transpose_model_matrix = zm.transpose(zm.inverse(model_matrix)); + queue.writeBuffer( + app.model_uniform_buffer, + @sizeOf(Mat4), + &invert_transpose_model_matrix, + ); + // Pass the surface size to shader to help sample from gBuffer textures using coord + const surface_size = Vec2{ screen_dimensions.width, screen_dimensions.height }; + queue.writeBuffer( + app.surface_size_uniform_buffer, + 0, + &surface_size, + ); +} + +fn buildCommandBuffer(app: *App) !*gpu.CommandBuffer { + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + defer back_buffer_view.release(); + + const encoder = core.device.createCommandEncoder(null); + defer encoder.release(); + + std.debug.assert(app.screen_dimensions.width == core.descriptor.width); + std.debug.assert(app.screen_dimensions.height == core.descriptor.height); + + const dimensions = Dimensions2D(f32){ + .width = @as(f32, @floatFromInt(core.descriptor.width)), + .height = @as(f32, @floatFromInt(core.descriptor.height)), + }; + + { + // Write position, normal, albedo etc. data to gBuffers + const pass = encoder.beginRenderPass(&app.write_gbuffer_pass.descriptor); + pass.setViewport( + 0, + 0, + dimensions.width, + dimensions.height, + 0.0, + 1.0, + ); + pass.setScissorRect(0, 0, core.descriptor.width, core.descriptor.height); + pass.setPipeline(app.write_gbuffers_pipeline); + pass.setBindGroup(0, app.scene_uniform_bind_group, null); + pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * app.vertex_count); + pass.setIndexBuffer(app.index_buffer, .uint16, 0, @sizeOf(u16) * app.index_count); + pass.drawIndexed( + app.index_count, + 1, // instance_count + 0, // first_index + 0, // base_vertex + 0, // first_instance + ); + pass.end(); + pass.release(); + } + { + // Update lights position + const pass = encoder.beginComputePass(null); + pass.setPipeline(app.light_update_compute_pipeline); + pass.setBindGroup(0, app.lights.buffer_compute_bind_group, null); + pass.dispatchWorkgroups(@divExact(max_num_lights, 64), 1, 1); + pass.end(); + pass.release(); + } + app.texture_quad_pass.color_attachment.view = back_buffer_view; + app.texture_quad_pass.descriptor = gpu.RenderPassDescriptor{ + .label = "texture_quad_pass(0)", + .color_attachment_count = 1, + .color_attachments = &[_]gpu.RenderPassColorAttachment{app.texture_quad_pass.color_attachment}, + }; + + const pass = encoder.beginRenderPass(&app.texture_quad_pass.descriptor); + switch (app.settings.render_mode) { + .gbuffer_view => { + // GBuffers debug view + // Left: position + // Middle: normal + // Right: albedo (use uv to mimic a checkerboard texture) + pass.setPipeline(app.gbuffers_debug_view_pipeline); + pass.setBindGroup(0, app.gbuffer_textures_bind_group, null); + pass.setBindGroup(1, app.surface_size_uniform_bind_group, null); + pass.draw(6, 1, 0, 0); + }, + else => { + // Deferred rendering + pass.setPipeline(app.deferred_render_pipeline); + pass.setBindGroup(0, app.gbuffer_textures_bind_group, null); + pass.setBindGroup(1, app.lights.buffer_bind_group, null); + pass.setBindGroup(2, app.surface_size_uniform_bind_group, null); + pass.draw(6, 1, 0, 0); + }, + } + + pass.end(); + pass.release(); + + return encoder.finish(null); +} + +const modes = [_][:0]const u8{ "rendering", "gbuffers view" }; + +fn printControls(app: *App) void { + std.debug.print("[controls]\n", .{}); + std.debug.print("[p] paused: {}\n", .{app.is_paused}); + std.debug.print("[m] mode: {s}\n", .{modes[@intFromEnum(app.settings.render_mode)]}); + std.debug.print("[,] decrease lights: {}\n", .{app.settings.lights_count}); + std.debug.print("[.] increase lights: {}\n", .{app.settings.lights_count}); +} + +fn updateUI(app: *App, event: core.Event) void { + switch (event) { + .key_press => |ev| { + var update_lights = false; + switch (ev.key) { + .p => app.is_paused = !app.is_paused, + .m => { + const mode_index = @intFromEnum(app.settings.render_mode); + app.settings.render_mode = @enumFromInt((mode_index + 1) % modes.len); + }, + .comma => { + update_lights = true; + if (app.settings.lights_count >= 25) app.settings.lights_count -= 25; + }, + .period => { + update_lights = true; + app.settings.lights_count += 25; + }, + else => return, + } + + if (update_lights) core.queue.writeBuffer( + app.lights.config_uniform_buffer, + 0, + &[1]i32{app.settings.lights_count}, + ); + app.printControls(); + }, + else => {}, + } +} + +// TODO +// fn drawUI(app: *App) void { +// if (imgui.beginCombo("Mode", .{ .preview_value = modes[mode_index] })) { +// for (modes, 0..) |mode, mode_i| { +// const i = @as(u32, @intCast(mode_i)); +// if (imgui.selectable(mode, .{ .selected = mode_index == i })) { +// app.settings.render_mode = @as(RenderMode, @enumFromInt(mode_i)); +// } +// } +// } +// if (imgui.sliderInt("Light count", .{ .v = &app.settings.lights_count, .min = 1, .max = max_num_lights })) { +// queue.writeBuffer( +// app.lights.config_uniform_buffer, +// 0, +// &[1]i32{app.settings.lights_count}, +// ); +// } +// imgui.end(); +// } + +fn updateUniformBuffers(app: *App) void { + core.device.tick(); + app.camera_rotation += toRadians(360.0) * (app.delta_time / 5.0); // one rotation every 5s + const rotation = zm.rotationY(app.camera_rotation); + const eye_position = zm.mul(rotation, zm.Vec{ 0, 50, -100, 0 }); + const view_matrix = zm.lookAtRh(eye_position, app.view_matrices.origin, app.view_matrices.up_vector); + app.view_matrices.view_proj_matrix = zm.mul(view_matrix, app.view_matrices.projection_matrix); + const queue = core.queue; + queue.writeBuffer( + app.camera_uniform_buffer, + 0, + &app.view_matrices.view_proj_matrix, + ); + + const inv_view_proj_matrix = zm.inverse(app.view_matrices.view_proj_matrix); + queue.writeBuffer( + app.camera_uniform_buffer, + @sizeOf(Mat4), + &inv_view_proj_matrix, + ); +} + +inline fn roundToMultipleOf4(comptime T: type, value: T) T { + return (value + 3) & ~@as(T, 3); +} + +inline fn toRadians(degrees: f32) f32 { + return degrees * (std.math.pi / 180.0); +} diff --git a/src/core/examples/sysgpu/deferred-rendering/vertexTextureQuad.wgsl b/src/core/examples/sysgpu/deferred-rendering/vertexTextureQuad.wgsl new file mode 100644 index 00000000..d906868b --- /dev/null +++ b/src/core/examples/sysgpu/deferred-rendering/vertexTextureQuad.wgsl @@ -0,0 +1,12 @@ +@vertex +fn main( + @builtin(vertex_index) VertexIndex : u32 +) -> @builtin(position) vec4 { + // TODO - array initialization + var pos = array, 6>( + vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0), + vec2(-1.0, 1.0), vec2(1.0, -1.0), vec2(1.0, 1.0), + ); + + return vec4(pos[VertexIndex], 0.0, 1.0); +} diff --git a/src/core/examples/sysgpu/deferred-rendering/vertexWriteGBuffers.wgsl b/src/core/examples/sysgpu/deferred-rendering/vertexWriteGBuffers.wgsl new file mode 100644 index 00000000..eb3b930e --- /dev/null +++ b/src/core/examples/sysgpu/deferred-rendering/vertexWriteGBuffers.wgsl @@ -0,0 +1,30 @@ +struct Uniforms { + modelMatrix : mat4x4, + normalModelMatrix : mat4x4, +} +struct Camera { + viewProjectionMatrix : mat4x4, + invViewProjectionMatrix : mat4x4, +} +@group(0) @binding(0) var uniforms : Uniforms; +@group(0) @binding(1) var camera : Camera; + +struct VertexOutput { + @builtin(position) Position : vec4, + @location(0) fragNormal: vec3, // normal in world space + @location(1) fragUV: vec2, +} + +@vertex +fn main( + @location(0) position : vec3, + @location(1) normal : vec3, + @location(2) uv : vec2 +) -> VertexOutput { + var output : VertexOutput; + let worldPosition = (uniforms.modelMatrix * vec4(position, 1.0)).xyz; + output.Position = camera.viewProjectionMatrix * vec4(worldPosition, 1.0); + output.fragNormal = normalize((uniforms.normalModelMatrix * vec4(normal, 1.0)).xyz); + output.fragUV = uv; + return output; +} diff --git a/src/core/examples/sysgpu/deferred-rendering/vertex_writer.zig b/src/core/examples/sysgpu/deferred-rendering/vertex_writer.zig new file mode 100644 index 00000000..1610981e --- /dev/null +++ b/src/core/examples/sysgpu/deferred-rendering/vertex_writer.zig @@ -0,0 +1,188 @@ +const std = @import("std"); + +/// Vertex writer manages the placement of vertices by tracking which are unique. If a duplicate vertex is added +/// with `put`, only it's index will be written to the index buffer. +/// `IndexType` should match the integer type used for the index buffer +pub fn VertexWriter(comptime VertexType: type, comptime IndexType: type) type { + return struct { + const MapEntry = struct { + packed_index: IndexType = null_index, + next_sparse: IndexType = null_index, + }; + + const null_index: IndexType = std.math.maxInt(IndexType); + + vertices: []VertexType, + indices: []IndexType, + sparse_to_packed_map: []MapEntry, + + /// Next index outside of the 1:1 mapping range for storing + /// position -> normal collisions + next_collision_index: IndexType, + + /// Next packed index + next_packed_index: IndexType, + written_indices_count: IndexType, + + /// Allocate storage and set default values + /// `sparse_vertices_count` is the number of vertices in the source before de-duplication / remapping + /// Put more succinctly, the largest index value in source index buffer + /// `max_vertex_count` is largest permutation of vertices assuming that {vertex, uv, normal} never map 1:1 and always + /// create a new mapping + pub fn init( + allocator: std.mem.Allocator, + indices_count: IndexType, + sparse_vertices_count: IndexType, + max_vertex_count: IndexType, + ) !@This() { + var result: @This() = undefined; + result.vertices = try allocator.alloc(VertexType, max_vertex_count); + result.indices = try allocator.alloc(IndexType, indices_count); + result.sparse_to_packed_map = try allocator.alloc(MapEntry, max_vertex_count); + result.next_collision_index = sparse_vertices_count; + result.next_packed_index = 0; + result.written_indices_count = 0; + @memset(result.sparse_to_packed_map, .{}); + return result; + } + + pub fn put(self: *@This(), vertex: VertexType, sparse_index: IndexType) void { + if (self.sparse_to_packed_map[sparse_index].packed_index == null_index) { + // New start of chain, reserve a new packed index and add entry to `index_map` + const packed_index = self.next_packed_index; + self.sparse_to_packed_map[sparse_index].packed_index = packed_index; + self.vertices[packed_index] = vertex; + self.indices[self.written_indices_count] = packed_index; + self.written_indices_count += 1; + self.next_packed_index += 1; + return; + } + var previous_sparse_index: IndexType = undefined; + var current_sparse_index = sparse_index; + while (current_sparse_index != null_index) { + const packed_index = self.sparse_to_packed_map[current_sparse_index].packed_index; + if (std.mem.eql(u8, &std.mem.toBytes(self.vertices[packed_index]), &std.mem.toBytes(vertex))) { + // We already have a record for this vertex in our chain + self.indices[self.written_indices_count] = packed_index; + self.written_indices_count += 1; + return; + } + previous_sparse_index = current_sparse_index; + current_sparse_index = self.sparse_to_packed_map[current_sparse_index].next_sparse; + } + // This is a new mapping for the given sparse index + const packed_index = self.next_packed_index; + const remapped_sparse_index = self.next_collision_index; + self.indices[self.written_indices_count] = packed_index; + self.vertices[packed_index] = vertex; + self.sparse_to_packed_map[previous_sparse_index].next_sparse = remapped_sparse_index; + self.sparse_to_packed_map[remapped_sparse_index].packed_index = packed_index; + self.next_packed_index += 1; + self.next_collision_index += 1; + self.written_indices_count += 1; + } + + pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void { + allocator.free(self.vertices); + allocator.free(self.indices); + allocator.free(self.sparse_to_packed_map); + } + + pub fn indexBuffer(self: @This()) []IndexType { + return self.indices; + } + + pub fn vertexBuffer(self: @This()) []VertexType { + return self.vertices[0..self.next_packed_index]; + } + }; +} + +test "VertexWriter" { + const Vec3 = [3]f32; + const Vertex = extern struct { + position: Vec3, + normal: Vec3, + }; + + const expect = std.testing.expect; + const allocator = std.testing.allocator; + + const Face = struct { + position: [3]u16, + normal: [3]u16, + }; + + const vertices = [_]Vec3{ + Vec3{ 1.0, 0.0, 0.0 }, // 0: Position + Vec3{ 2.0, 0.0, 0.0 }, // 1: Position + Vec3{ 3.0, 0.0, 0.0 }, // 2: Position + Vec3{ 1.0, 0.0, 0.0 }, // 3: Normal + Vec3{ 4.0, 0.0, 0.0 }, // 4: Position + Vec3{ 0.0, 1.0, 0.0 }, // 5: Normal + Vec3{ 5.0, 0.0, 0.0 }, // 6: Position + Vec3{ 0.0, 0.0, 1.0 }, // 7: Normal + Vec3{ 1.0, 0.0, 1.0 }, // 8: Normal + Vec3{ 6.0, 0.0, 0.0 }, // 9: Position + }; + + const faces = [_]Face{ + .{ .position = .{ 0, 4, 2 }, .normal = .{ 7, 5, 3 } }, + .{ .position = .{ 2, 3, 9 }, .normal = .{ 3, 7, 8 } }, + .{ .position = .{ 9, 2, 4 }, .normal = .{ 8, 7, 5 } }, + .{ .position = .{ 2, 6, 1 }, .normal = .{ 3, 5, 7 } }, + .{ .position = .{ 9, 6, 0 }, .normal = .{ 5, 7, 8 } }, + }; + + var writer = try VertexWriter(Vertex, u32).init( + allocator, + faces.len * 3, // indices count + vertices.len, // original vertices count + faces.len * 3, // maximum vertices count + ); + defer writer.deinit(allocator); + + for (faces) |face| { + var x: usize = 0; + while (x < 3) : (x += 1) { + const position_index = face.position[x]; + const position = vertices[position_index]; + const normal = vertices[face.normal[x]]; + const vertex = Vertex{ + .position = position, + .normal = normal, + }; + writer.put(vertex, position_index); + } + } + + const indices = writer.indexBuffer(); + try expect(indices.len == faces.len * 3); + + // Face 0 + try expect(indices[0] == 0); // (0, 7) New + try expect(indices[1] == 1); // (4, 5) New + try expect(indices[2] == 2); // (2, 3) New + + // Face 1 + try expect(indices[3 + 0] == 2); // (2, 3) Duplicate - Reuse index + try expect(indices[3 + 1] == 3); // (3, 7) New + try expect(indices[3 + 2] == 4); // (9, 8) New + + // Face 2 + try expect(indices[6 + 0] == 4); // (9, 8) Duplicate - Reuse index + try expect(indices[6 + 1] == 5); // (2, 7) New normal mapping (Don't clobber) + try expect(indices[6 + 2] == 1); // (4, 5) Duplicate - Reuse Index + + // Face 3 + try expect(indices[9 + 0] == 2); // (2, 3) Duplicate - Reuse index + try expect(indices[9 + 1] == 6); // (6, 5) New + try expect(indices[9 + 2] == 7); // (1, 7) New + + // Face 4 + try expect(indices[12 + 0] == 8); // (9, 5) New normal mapping (Don't clobber) + try expect(indices[12 + 1] == 9); // (6, 7) New normal mapping (Don't clobber) + try expect(indices[12 + 2] == 10); // (0, 8) New normal mapping (Don't clobber) + + try expect(writer.vertexBuffer().len == 11); +} diff --git a/src/core/examples/sysgpu/fractal-cube/cube_mesh.zig b/src/core/examples/sysgpu/fractal-cube/cube_mesh.zig new file mode 100644 index 00000000..f26c75ac --- /dev/null +++ b/src/core/examples/sysgpu/fractal-cube/cube_mesh.zig @@ -0,0 +1,49 @@ +pub const Vertex = extern struct { + pos: @Vector(4, f32), + col: @Vector(4, f32), + uv: @Vector(2, f32), +}; + +pub const vertices = [_]Vertex{ + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, +}; diff --git a/src/core/examples/sysgpu/fractal-cube/main.zig b/src/core/examples/sysgpu/fractal-cube/main.zig new file mode 100644 index 00000000..d18b7413 --- /dev/null +++ b/src/core/examples/sysgpu/fractal-cube/main.zig @@ -0,0 +1,376 @@ +//! To get the effect we want, we need a texture on which to render; +//! we can't use the swapchain texture directly, but we can get the effect +//! by doing the same render pass twice, on the texture and the swapchain. +//! We also need a second texture to use on the cube (after the render pass +//! it needs to copy the other texture.) We can't use the same texture since +//! it would interfere with the synchronization on the gpu during the render pass. +//! This demo currently does not work on opengl, because core.descriptor.width/height, +//! are set to 0 after core.init() and because webgpu does not implement copyTextureToTexture, +//! for opengl + +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const zm = @import("zmath"); +const Vertex = @import("cube_mesh.zig").Vertex; +const vertices = @import("cube_mesh.zig").vertices; + +pub const App = @This(); + +pub const mach_core_options = core.ComptimeOptions{ + .use_wgpu = false, + .use_sysgpu = true, +}; + +const UniformBufferObject = struct { + mat: zm.Mat, +}; + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +title_timer: core.Timer, +timer: core.Timer, +pipeline: *gpu.RenderPipeline, +vertex_buffer: *gpu.Buffer, +uniform_buffer: *gpu.Buffer, +bind_group: *gpu.BindGroup, +depth_texture: ?*gpu.Texture, +depth_texture_view: *gpu.TextureView, +cube_texture: *gpu.Texture, +cube_texture_view: *gpu.TextureView, +cube_texture_render: *gpu.Texture, +cube_texture_view_render: *gpu.TextureView, +sampler: *gpu.Sampler, +bgl: *gpu.BindGroupLayout, + +pub fn init(app: *App) !void { + try core.init(.{}); + + const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); + + const vertex_attributes = [_]gpu.VertexAttribute{ + .{ .format = .float32x4, .offset = @offsetOf(Vertex, "pos"), .shader_location = 0 }, + .{ .format = .float32x2, .offset = @offsetOf(Vertex, "uv"), .shader_location = 1 }, + }; + const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{ + .array_stride = @sizeOf(Vertex), + .attributes = &vertex_attributes, + }); + + const blend = gpu.BlendState{}; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + + const bgle_buffer = gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, true, 0); + const bgle_sampler = gpu.BindGroupLayout.Entry.sampler(1, .{ .fragment = true }, .filtering); + const bgle_textureview = gpu.BindGroupLayout.Entry.texture(2, .{ .fragment = true }, .float, .dimension_2d, false); + const bgl = core.device.createBindGroupLayout( + &gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &.{ bgle_buffer, bgle_sampler, bgle_textureview }, + }), + ); + + const bind_group_layouts = [_]*gpu.BindGroupLayout{bgl}; + const pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &bind_group_layouts, + })); + + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + .layout = pipeline_layout, + .depth_stencil = &.{ + .format = .depth24_plus, + .depth_write_enabled = .true, + .depth_compare = .less, + }, + .vertex = gpu.VertexState.init(.{ + .module = shader_module, + .entry_point = "vertex_main", + .buffers = &.{vertex_buffer_layout}, + }), + .primitive = .{ + .cull_mode = .back, + }, + }; + + const vertex_buffer = core.device.createBuffer(&.{ + .usage = .{ .vertex = true }, + .size = @sizeOf(Vertex) * vertices.len, + .mapped_at_creation = .true, + }); + const vertex_mapped = vertex_buffer.getMappedRange(Vertex, 0, vertices.len); + @memcpy(vertex_mapped.?, vertices[0..]); + vertex_buffer.unmap(); + + const uniform_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = @sizeOf(UniformBufferObject), + .mapped_at_creation = .false, + }); + + // The texture to put on the cube + const cube_texture = core.device.createTexture(&gpu.Texture.Descriptor{ + .usage = .{ .texture_binding = true, .copy_dst = true }, + .size = .{ .width = core.descriptor.width, .height = core.descriptor.height }, + .format = core.descriptor.format, + }); + // The texture on which we render + const cube_texture_render = core.device.createTexture(&gpu.Texture.Descriptor{ + .usage = .{ .render_attachment = true, .copy_src = true }, + .size = .{ .width = core.descriptor.width, .height = core.descriptor.height }, + .format = core.descriptor.format, + }); + + const sampler = core.device.createSampler(&gpu.Sampler.Descriptor{ + .mag_filter = .linear, + .min_filter = .linear, + }); + + const cube_texture_view = cube_texture.createView(&gpu.TextureView.Descriptor{ + .format = core.descriptor.format, + .dimension = .dimension_2d, + .mip_level_count = 1, + .array_layer_count = 1, + }); + const cube_texture_view_render = cube_texture_render.createView(&gpu.TextureView.Descriptor{ + .format = core.descriptor.format, + .dimension = .dimension_2d, + .mip_level_count = 1, + .array_layer_count = 1, + }); + + const bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bgl, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, uniform_buffer, 0, @sizeOf(UniformBufferObject), @sizeOf(UniformBufferObject)), + gpu.BindGroup.Entry.sampler(1, sampler), + gpu.BindGroup.Entry.textureView(2, cube_texture_view), + }, + }), + ); + + const depth_texture = core.device.createTexture(&gpu.Texture.Descriptor{ + .usage = .{ .render_attachment = true }, + .size = .{ .width = core.descriptor.width, .height = core.descriptor.height }, + .format = .depth24_plus, + }); + const depth_texture_view = depth_texture.createView(&gpu.TextureView.Descriptor{ + .format = .depth24_plus, + .dimension = .dimension_2d, + .array_layer_count = 1, + .mip_level_count = 1, + }); + + app.timer = try core.Timer.start(); + app.title_timer = try core.Timer.start(); + app.pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + app.vertex_buffer = vertex_buffer; + app.uniform_buffer = uniform_buffer; + app.bind_group = bind_group; + app.depth_texture = depth_texture; + app.depth_texture_view = depth_texture_view; + app.cube_texture = cube_texture; + app.cube_texture_view = cube_texture_view; + app.cube_texture_render = cube_texture_render; + app.cube_texture_view_render = cube_texture_view_render; + app.sampler = sampler; + app.bgl = bgl; + + shader_module.release(); + pipeline_layout.release(); +} + +pub fn deinit(app: *App) void { + defer _ = gpa.deinit(); + defer core.deinit(); + + app.pipeline.release(); + app.bgl.release(); + app.vertex_buffer.release(); + app.uniform_buffer.release(); + app.cube_texture.release(); + app.cube_texture_render.release(); + app.sampler.release(); + app.cube_texture_view.release(); + app.cube_texture_view_render.release(); + app.bind_group.release(); + app.depth_texture.?.release(); + app.depth_texture_view.release(); +} + +pub fn update(app: *App) !bool { + var iter = core.pollEvents(); + while (iter.next()) |event| { + switch (event) { + .key_press => |ev| { + if (ev.key == .space) return true; + }, + .close => return true, + .framebuffer_resize => |ev| { + app.depth_texture.?.release(); + app.depth_texture = core.device.createTexture(&gpu.Texture.Descriptor{ + .usage = .{ .render_attachment = true }, + .size = .{ .width = ev.width, .height = ev.height }, + .format = .depth24_plus, + }); + + app.cube_texture.release(); + app.cube_texture = core.device.createTexture(&gpu.Texture.Descriptor{ + .usage = .{ .texture_binding = true, .copy_dst = true }, + .size = .{ .width = ev.width, .height = ev.height }, + .format = core.descriptor.format, + }); + app.cube_texture_render.release(); + app.cube_texture_render = core.device.createTexture(&gpu.Texture.Descriptor{ + .usage = .{ .render_attachment = true, .copy_src = true }, + .size = .{ .width = ev.width, .height = ev.height }, + .format = core.descriptor.format, + }); + + app.depth_texture_view.release(); + app.depth_texture_view = app.depth_texture.?.createView(&gpu.TextureView.Descriptor{ + .format = .depth24_plus, + .dimension = .dimension_2d, + .array_layer_count = 1, + .mip_level_count = 1, + }); + + app.cube_texture_view.release(); + app.cube_texture_view = app.cube_texture.createView(&gpu.TextureView.Descriptor{ + .format = core.descriptor.format, + .dimension = .dimension_2d, + .mip_level_count = 1, + .array_layer_count = 1, + }); + app.cube_texture_view_render.release(); + app.cube_texture_view_render = app.cube_texture_render.createView(&gpu.TextureView.Descriptor{ + .format = core.descriptor.format, + .dimension = .dimension_2d, + .mip_level_count = 1, + .array_layer_count = 1, + }); + + app.bind_group.release(); + app.bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = app.bgl, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, app.uniform_buffer, 0, @sizeOf(UniformBufferObject), @sizeOf(UniformBufferObject)), + gpu.BindGroup.Entry.sampler(1, app.sampler), + gpu.BindGroup.Entry.textureView(2, app.cube_texture_view), + }, + }), + ); + }, + else => {}, + } + } + + const cube_view = app.cube_texture_view_render; + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + + const cube_color_attachment = gpu.RenderPassColorAttachment{ + .view = cube_view, + .clear_value = gpu.Color{ .r = 0.5, .g = 0.5, .b = 0.5, .a = 1 }, + .load_op = .clear, + .store_op = .store, + }; + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = gpu.Color{ .r = 0.5, .g = 0.5, .b = 0.5, .a = 1 }, + .load_op = .clear, + .store_op = .store, + }; + + const depth_stencil_attachment = gpu.RenderPassDepthStencilAttachment{ + .view = app.depth_texture_view, + .depth_load_op = .clear, + .depth_store_op = .store, + .depth_clear_value = 1.0, + }; + + const encoder = core.device.createCommandEncoder(null); + const cube_render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{cube_color_attachment}, + .depth_stencil_attachment = &depth_stencil_attachment, + }); + const render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + .depth_stencil_attachment = &depth_stencil_attachment, + }); + + { + const time = app.timer.read(); + const model = zm.mul(zm.rotationX(time * (std.math.pi / 2.0)), zm.rotationZ(time * (std.math.pi / 2.0))); + const view = zm.lookAtRh( + zm.Vec{ 0, -4, 0, 1 }, + zm.Vec{ 0, 0, 0, 1 }, + zm.Vec{ 0, 0, 1, 0 }, + ); + const proj = zm.perspectiveFovRh( + (std.math.pi * 2.0 / 5.0), + @as(f32, @floatFromInt(core.descriptor.width)) / @as(f32, @floatFromInt(core.descriptor.height)), + 1, + 100, + ); + const ubo = UniformBufferObject{ + .mat = zm.transpose(zm.mul(zm.mul(model, view), proj)), + }; + encoder.writeBuffer(app.uniform_buffer, 0, &[_]UniformBufferObject{ubo}); + } + + const pass = encoder.beginRenderPass(&render_pass_info); + pass.setPipeline(app.pipeline); + pass.setBindGroup(0, app.bind_group, &.{0}); + pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len); + pass.draw(vertices.len, 1, 0, 0); + pass.end(); + pass.release(); + + encoder.copyTextureToTexture( + &gpu.ImageCopyTexture{ + .texture = app.cube_texture_render, + }, + &gpu.ImageCopyTexture{ + .texture = app.cube_texture, + }, + &.{ .width = core.descriptor.width, .height = core.descriptor.height }, + ); + + const cube_pass = encoder.beginRenderPass(&cube_render_pass_info); + cube_pass.setPipeline(app.pipeline); + cube_pass.setBindGroup(0, app.bind_group, &.{0}); + cube_pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len); + cube_pass.draw(vertices.len, 1, 0, 0); + cube_pass.end(); + cube_pass.release(); + + var command = encoder.finish(null); + encoder.release(); + + const queue = core.queue; + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Fractal Cube [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} diff --git a/src/core/examples/sysgpu/fractal-cube/shader.wgsl b/src/core/examples/sysgpu/fractal-cube/shader.wgsl new file mode 100644 index 00000000..d38f0b4e --- /dev/null +++ b/src/core/examples/sysgpu/fractal-cube/shader.wgsl @@ -0,0 +1,36 @@ +struct Uniforms { + matrix : mat4x4, +}; + +@binding(0) @group(0) var ubo : Uniforms; + +struct VertexOut { + @builtin(position) Position : vec4, + @location(0) fragUV : vec2, + @location(1) fragPosition: vec4, +} + +@vertex fn vertex_main( + @location(0) position : vec4, + @location(1) uv: vec2 +) -> VertexOut { + var output : VertexOut; + output.Position = position * ubo.matrix; + output.fragUV = uv; + output.fragPosition = 0.5 * (position + vec4(1.0, 1.0, 1.0, 1.0)); + return output; +} + +@binding(1) @group(0) var mySampler: sampler; +@binding(2) @group(0) var myTexture: texture_2d; + +@fragment fn frag_main( + @location(0) fragUV: vec2, + @location(1) fragPosition: vec4 +) -> @location(0) vec4 { + let texColor = textureSample(myTexture, mySampler, fragUV * 0.8 + vec2(0.1, 0.1)); + let f = f32(length(texColor.rgb - vec3(0.5, 0.5, 0.5)) < 0.01); + return (1.0 - f) * texColor + f * fragPosition; + // return vec4(texColor.rgb,1.0); +} + diff --git a/src/core/examples/sysgpu/gen-texture-light/cube.wgsl b/src/core/examples/sysgpu/gen-texture-light/cube.wgsl new file mode 100644 index 00000000..bc59632d --- /dev/null +++ b/src/core/examples/sysgpu/gen-texture-light/cube.wgsl @@ -0,0 +1,71 @@ +struct CameraUniform { + pos: vec4, + view_proj: mat4x4, +}; + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) tex_coords: vec2, + @location(1) normal: vec3, + @location(2) position: vec3, +}; + +struct Light { + position: vec4, + color: vec4, +}; + +@group(0) @binding(0) var camera: CameraUniform; +@group(1) @binding(0) var t_diffuse: texture_2d; +@group(1) @binding(1) var s_diffuse: sampler; +@group(2) @binding(0) var light: Light; + +@vertex +fn vs_main( + // TODO - struct input + @location(0) model_position: vec3, + @location(1) model_normal: vec3, + @location(2) model_tex_coords: vec2, + @location(3) instance_model_matrix_0: vec4, + @location(4) instance_model_matrix_1: vec4, + @location(5) instance_model_matrix_2: vec4, + @location(6) instance_model_matrix_3: vec4) +-> VertexOutput { + let model_matrix = mat4x4( + instance_model_matrix_0, + instance_model_matrix_1, + instance_model_matrix_2, + instance_model_matrix_3, + ); + var out: VertexOutput; + let world_pos = model_matrix * vec4(model_position, 1.0); + out.position = world_pos.xyz; + out.normal = (model_matrix * vec4(model_normal, 0.0)).xyz; + out.clip_position = camera.view_proj * world_pos; + out.tex_coords = model_tex_coords; + return out; +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + let object_color = textureSample(t_diffuse, s_diffuse, in.tex_coords); + + let ambient = 0.1; + let ambient_color = light.color.rbg * ambient; + + let light_dir = normalize(light.position.xyz - in.position); + let diffuse = max(dot(in.normal, light_dir), 0.0); + let diffuse_color = light.color.rgb * diffuse; + + let view_dir = normalize(camera.pos.xyz - in.position); + let half_dir = normalize(view_dir + light_dir); + let specular = pow(max(dot(in.normal, half_dir), 0.0), 32.0); + let specular_color = light.color.rbg * specular; + + let all = ambient_color + diffuse_color + specular_color; + + let result = all * object_color.rgb; + + return vec4(result, object_color.a); + +} diff --git a/src/core/examples/sysgpu/gen-texture-light/light.wgsl b/src/core/examples/sysgpu/gen-texture-light/light.wgsl new file mode 100644 index 00000000..a272ccf0 --- /dev/null +++ b/src/core/examples/sysgpu/gen-texture-light/light.wgsl @@ -0,0 +1,34 @@ +struct CameraUniform { + view_pos: vec4, + view_proj: mat4x4, +}; + +struct VertexOutput { + @builtin(position) clip_position: vec4, +}; + +struct Light { + position: vec4, + color: vec4, +}; + +@group(0) @binding(0) var camera: CameraUniform; +@group(1) @binding(0) var light: Light; + +@vertex +fn vs_main( + // TODO - struct input + @location(0) model_position: vec3, + @location(1) model_normal: vec3, + @location(2) model_tex_coords: vec2, +) -> VertexOutput { + var out: VertexOutput; + let world_pos = vec4(model_position + light.position.xyz, 1.0); + out.clip_position = camera.view_proj * world_pos; + return out; +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + return vec4(1.0, 1.0, 1.0, 0.5); +} diff --git a/src/core/examples/sysgpu/gen-texture-light/main.zig b/src/core/examples/sysgpu/gen-texture-light/main.zig new file mode 100644 index 00000000..20a58ad5 --- /dev/null +++ b/src/core/examples/sysgpu/gen-texture-light/main.zig @@ -0,0 +1,896 @@ +// in this example: +// - comptime generated image data for texture +// - Blinn-Phong lighting +// - several pipelines +// +// quit with escape, q or space +// move camera with arrows or wasd + +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const zm = @import("zmath"); + +const Vec = zm.Vec; +const Mat = zm.Mat; +const Quat = zm.Quat; + +pub const App = @This(); + +pub const mach_core_options = core.ComptimeOptions{ + .use_wgpu = false, + .use_sysgpu = true, +}; + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +title_timer: core.Timer, +timer: core.Timer, +cube: Cube, +camera: Camera, +light: Light, +depth: Texture, +keys: u8, + +const Dir = struct { + const up: u8 = 0b0001; + const down: u8 = 0b0010; + const left: u8 = 0b0100; + const right: u8 = 0b1000; +}; + +pub fn init(app: *App) !void { + try core.init(.{}); + + app.title_timer = try core.Timer.start(); + app.timer = try core.Timer.start(); + + const eye = Vec{ 5.0, 7.0, 5.0, 0.0 }; + const target = Vec{ 0.0, 0.0, 0.0, 0.0 }; + + const framebuffer = core.descriptor; + const aspect_ratio = @as(f32, @floatFromInt(framebuffer.width)) / @as(f32, @floatFromInt(framebuffer.height)); + + app.cube = Cube.init(); + app.light = Light.init(); + app.depth = Texture.depth(core.device, framebuffer.width, framebuffer.height); + app.camera = Camera.init(core.device, eye, target, zm.Vec{ 0.0, 1.0, 0.0, 0.0 }, aspect_ratio, 45.0, 0.1, 100.0); + app.keys = 0; +} + +pub fn deinit(app: *App) void { + defer _ = gpa.deinit(); + defer core.deinit(); + + app.cube.deinit(); + app.camera.deinit(); + app.light.deinit(); + app.depth.release(); +} + +pub fn update(app: *App) !bool { + const delta_time = app.timer.lap(); + + var iter = core.pollEvents(); + while (iter.next()) |event| { + switch (event) { + .key_press => |ev| switch (ev.key) { + .q, .escape, .space => return true, + .w, .up => { + app.keys |= Dir.up; + }, + .s, .down => { + app.keys |= Dir.down; + }, + .a, .left => { + app.keys |= Dir.left; + }, + .d, .right => { + app.keys |= Dir.right; + }, + .one => core.setDisplayMode(.windowed), + .two => core.setDisplayMode(.fullscreen), + .three => core.setDisplayMode(.borderless), + else => {}, + }, + .key_release => |ev| switch (ev.key) { + .w, .up => { + app.keys &= ~Dir.up; + }, + .s, .down => { + app.keys &= ~Dir.down; + }, + .a, .left => { + app.keys &= ~Dir.left; + }, + .d, .right => { + app.keys &= ~Dir.right; + }, + else => {}, + }, + .framebuffer_resize => |ev| { + // recreates the sampler, which is a waste, but for an example it's ok + app.depth.release(); + app.depth = Texture.depth(core.device, ev.width, ev.height); + }, + .close => return true, + else => {}, + } + } + + // move camera + const speed = zm.Vec{ delta_time * 5, delta_time * 5, delta_time * 5, delta_time * 5 }; + const fwd = zm.normalize3(app.camera.target - app.camera.eye); + const right = zm.normalize3(zm.cross3(fwd, app.camera.up)); + + if (app.keys & Dir.up != 0) + app.camera.eye += fwd * speed; + + if (app.keys & Dir.down != 0) + app.camera.eye -= fwd * speed; + + if (app.keys & Dir.right != 0) + app.camera.eye += right * speed + else if (app.keys & Dir.left != 0) + app.camera.eye -= right * speed + else + app.camera.eye += right * (speed * @Vector(4, f32){ 0.5, 0.5, 0.5, 0.5 }); + + const queue = core.queue; + app.camera.update(queue); + + // move light + const light_speed = delta_time * 2.5; + app.light.update(queue, light_speed); + + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + defer back_buffer_view.release(); + + const encoder = core.device.createCommandEncoder(null); + defer encoder.release(); + + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = gpu.Color{ .r = 0.0, .g = 0.0, .b = 0.4, .a = 1.0 }, + .load_op = .clear, + .store_op = .store, + }; + + const render_pass_descriptor = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + .depth_stencil_attachment = &.{ + .view = app.depth.view, + .depth_load_op = .clear, + .depth_store_op = .store, + .depth_clear_value = 1.0, + }, + }); + + const pass = encoder.beginRenderPass(&render_pass_descriptor); + defer pass.release(); + + // brick cubes + pass.setPipeline(app.cube.pipeline); + pass.setBindGroup(0, app.camera.bind_group, &.{}); + pass.setBindGroup(1, app.cube.texture.bind_group.?, &.{}); + pass.setBindGroup(2, app.light.bind_group, &.{}); + pass.setVertexBuffer(0, app.cube.mesh.buffer, 0, app.cube.mesh.size); + pass.setVertexBuffer(1, app.cube.instance.buffer, 0, app.cube.instance.size); + pass.draw(4, app.cube.instance.len, 0, 0); + pass.draw(4, app.cube.instance.len, 4, 0); + pass.draw(4, app.cube.instance.len, 8, 0); + pass.draw(4, app.cube.instance.len, 12, 0); + pass.draw(4, app.cube.instance.len, 16, 0); + pass.draw(4, app.cube.instance.len, 20, 0); + + // light source + pass.setPipeline(app.light.pipeline); + pass.setBindGroup(0, app.camera.bind_group, &.{}); + pass.setBindGroup(1, app.light.bind_group, &.{}); + pass.setVertexBuffer(0, app.cube.mesh.buffer, 0, app.cube.mesh.size); + pass.draw(4, 1, 0, 0); + pass.draw(4, 1, 4, 0); + pass.draw(4, 1, 8, 0); + pass.draw(4, 1, 12, 0); + pass.draw(4, 1, 16, 0); + pass.draw(4, 1, 20, 0); + + pass.end(); + + var command = encoder.finish(null); + defer command.release(); + + queue.submit(&[_]*gpu.CommandBuffer{command}); + core.swap_chain.present(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Gen Texture Light [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} + +const Camera = struct { + const Self = @This(); + + eye: Vec, + target: Vec, + up: Vec, + aspect: f32, + fovy: f32, + near: f32, + far: f32, + bind_group: *gpu.BindGroup, + buffer: Buffer, + + const Uniform = extern struct { + pos: Vec, + mat: Mat, + }; + + fn init(device: *gpu.Device, eye: Vec, target: Vec, up: Vec, aspect: f32, fovy: f32, near: f32, far: f32) Self { + var self: Self = .{ + .eye = eye, + .target = target, + .up = up, + .aspect = aspect, + .near = near, + .far = far, + .fovy = fovy, + .buffer = undefined, + .bind_group = undefined, + }; + + const view = self.buildViewProjMatrix(); + + const uniform = Uniform{ + .pos = self.eye, + .mat = view, + }; + + const buffer = .{ + .buffer = initBuffer(device, .{ .uniform = true }, &@as([20]f32, @bitCast(uniform))), + .size = @sizeOf(@TypeOf(uniform)), + }; + + const layout = Self.bindGroupLayout(device); + const bind_group = device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{ + .layout = layout, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, buffer.buffer, 0, buffer.size, buffer.size), + }, + })); + layout.release(); + + self.buffer = buffer; + self.bind_group = bind_group; + + return self; + } + + fn deinit(self: *Self) void { + self.bind_group.release(); + self.buffer.release(); + } + + fn update(self: *Self, queue: *gpu.Queue) void { + const mat = self.buildViewProjMatrix(); + const uniform = .{ + .pos = self.eye, + .mat = mat, + }; + + queue.writeBuffer(self.buffer.buffer, 0, &[_]Uniform{uniform}); + } + + inline fn buildViewProjMatrix(s: *const Camera) Mat { + const view = zm.lookAtRh(s.eye, s.target, s.up); + const proj = zm.perspectiveFovRh(s.fovy, s.aspect, s.near, s.far); + return zm.mul(view, proj); + } + + inline fn bindGroupLayout(device: *gpu.Device) *gpu.BindGroupLayout { + const visibility = .{ .vertex = true, .fragment = true }; + return device.createBindGroupLayout(&gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &.{ + gpu.BindGroupLayout.Entry.buffer(0, visibility, .uniform, false, 0), + }, + })); + } +}; + +const Buffer = struct { + const Self = @This(); + + buffer: *gpu.Buffer, + size: usize, + len: u32 = 0, + + fn release(self: *Self) void { + self.buffer.release(); + } +}; + +const Cube = struct { + const Self = @This(); + + pipeline: *gpu.RenderPipeline, + mesh: Buffer, + instance: Buffer, + texture: Texture, + + const IPR = 20; // instances per row + const SPACING = 2; // spacing between cubes + const DISPLACEMENT = vec3u(IPR * SPACING / 2, 0, IPR * SPACING / 2); + + fn init() Self { + const device = core.device; + + const texture = Brick.texture(device); + + // instance buffer + var ibuf: [IPR * IPR * 16]f32 = undefined; + + var z: usize = 0; + while (z < IPR) : (z += 1) { + var x: usize = 0; + while (x < IPR) : (x += 1) { + const pos = vec3u(x * SPACING, 0, z * SPACING) - DISPLACEMENT; + const rot = blk: { + if (pos[0] == 0 and pos[2] == 0) { + break :blk zm.rotationZ(0.0); + } else { + break :blk zm.mul(zm.rotationX(zm.clamp(zm.abs(pos[0]), 0, 45.0)), zm.rotationZ(zm.clamp(zm.abs(pos[2]), 0, 45.0))); + } + }; + const index = z * IPR + x; + const inst = Instance{ + .position = pos, + .rotation = rot, + }; + zm.storeMat(ibuf[index * 16 ..], inst.toMat()); + } + } + + const instance = Buffer{ + .buffer = initBuffer(device, .{ .vertex = true }, &ibuf), + .len = IPR * IPR, + .size = @sizeOf(@TypeOf(ibuf)), + }; + + return Self{ + .mesh = mesh(device), + .texture = texture, + .instance = instance, + .pipeline = pipeline(), + }; + } + + fn deinit(self: *Self) void { + self.pipeline.release(); + self.mesh.release(); + self.instance.release(); + self.texture.release(); + } + + fn pipeline() *gpu.RenderPipeline { + const device = core.device; + + const camera_layout = Camera.bindGroupLayout(device); + const texture_layout = Texture.bindGroupLayout(device); + const light_layout = Light.bindGroupLayout(device); + const layout_descriptor = gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &.{ + camera_layout, + texture_layout, + light_layout, + }, + }); + defer camera_layout.release(); + defer texture_layout.release(); + defer light_layout.release(); + + const layout = device.createPipelineLayout(&layout_descriptor); + defer layout.release(); + + const shader = device.createShaderModuleWGSL("cube.wgsl", @embedFile("cube.wgsl")); + defer shader.release(); + + const blend = gpu.BlendState{}; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + }; + + const fragment = gpu.FragmentState.init(.{ + .module = shader, + .entry_point = "fs_main", + .targets = &.{color_target}, + }); + + const descriptor = gpu.RenderPipeline.Descriptor{ + .layout = layout, + .fragment = &fragment, + .vertex = gpu.VertexState.init(.{ + .module = shader, + .entry_point = "vs_main", + .buffers = &.{ + Self.vertexBufferLayout(), + Self.instanceLayout(), + }, + }), + .depth_stencil = &.{ + .format = Texture.DEPTH_FORMAT, + .depth_write_enabled = .true, + .depth_compare = .less, + }, + .primitive = .{ + .cull_mode = .back, + .topology = .triangle_strip, + }, + }; + + return device.createRenderPipeline(&descriptor); + } + + fn mesh(device: *gpu.Device) Buffer { + // generated texture has aspect ratio of 1:2 + // `h` reflects that ratio + // `v` sets how many times texture repeats across surface + const v = 2; + const h = v * 2; + const buf = asFloats(.{ + // z+ face + 0, 0, 1, 0, 0, 1, 0, h, + 1, 0, 1, 0, 0, 1, v, h, + 0, 1, 1, 0, 0, 1, 0, 0, + 1, 1, 1, 0, 0, 1, v, 0, + // z- face + 1, 0, 0, 0, 0, -1, 0, h, + 0, 0, 0, 0, 0, -1, v, h, + 1, 1, 0, 0, 0, -1, 0, 0, + 0, 1, 0, 0, 0, -1, v, 0, + // x+ face + 1, 0, 1, 1, 0, 0, 0, h, + 1, 0, 0, 1, 0, 0, v, h, + 1, 1, 1, 1, 0, 0, 0, 0, + 1, 1, 0, 1, 0, 0, v, 0, + // x- face + 0, 0, 0, -1, 0, 0, 0, h, + 0, 0, 1, -1, 0, 0, v, h, + 0, 1, 0, -1, 0, 0, 0, 0, + 0, 1, 1, -1, 0, 0, v, 0, + // y+ face + 1, 1, 0, 0, 1, 0, 0, h, + 0, 1, 0, 0, 1, 0, v, h, + 1, 1, 1, 0, 1, 0, 0, 0, + 0, 1, 1, 0, 1, 0, v, 0, + // y- face + 0, 0, 0, 0, -1, 0, 0, h, + 1, 0, 0, 0, -1, 0, v, h, + 0, 0, 1, 0, -1, 0, 0, 0, + 1, 0, 1, 0, -1, 0, v, 0, + }); + + return Buffer{ + .buffer = initBuffer(device, .{ .vertex = true }, &buf), + .size = @sizeOf(@TypeOf(buf)), + }; + } + + fn vertexBufferLayout() gpu.VertexBufferLayout { + const attributes = [_]gpu.VertexAttribute{ + .{ + .format = .float32x3, + .offset = 0, + .shader_location = 0, + }, + .{ + .format = .float32x3, + .offset = @sizeOf([3]f32), + .shader_location = 1, + }, + .{ + .format = .float32x2, + .offset = @sizeOf([6]f32), + .shader_location = 2, + }, + }; + return gpu.VertexBufferLayout.init(.{ + .array_stride = @sizeOf([8]f32), + .attributes = &attributes, + }); + } + + fn instanceLayout() gpu.VertexBufferLayout { + const attributes = [_]gpu.VertexAttribute{ + .{ + .format = .float32x4, + .offset = 0, + .shader_location = 3, + }, + .{ + .format = .float32x4, + .offset = @sizeOf([4]f32), + .shader_location = 4, + }, + .{ + .format = .float32x4, + .offset = @sizeOf([8]f32), + .shader_location = 5, + }, + .{ + .format = .float32x4, + .offset = @sizeOf([12]f32), + .shader_location = 6, + }, + }; + + return gpu.VertexBufferLayout.init(.{ + .array_stride = @sizeOf([16]f32), + .step_mode = .instance, + .attributes = &attributes, + }); + } +}; + +fn asFloats(comptime arr: anytype) [arr.len]f32 { + const len = arr.len; + comptime var out: [len]f32 = undefined; + comptime var i = 0; + inline while (i < len) : (i += 1) { + out[i] = @as(f32, @floatFromInt(arr[i])); + } + return out; +} + +const Brick = struct { + const W = 12; + const H = 6; + + fn texture(device: *gpu.Device) Texture { + const slice: []const u8 = &data(); + return Texture.fromData(device, W, H, u8, slice); + } + + fn data() [W * H * 4]u8 { + comptime var out: [W * H * 4]u8 = undefined; + + // fill all the texture with brick color + comptime var i = 0; + inline while (i < H) : (i += 1) { + comptime var j = 0; + inline while (j < W * 4) : (j += 4) { + out[i * W * 4 + j + 0] = 210; + out[i * W * 4 + j + 1] = 30; + out[i * W * 4 + j + 2] = 30; + out[i * W * 4 + j + 3] = 0; + } + } + + const f = 10; + + // fill the cement lines + inline for ([_]comptime_int{ 0, 1 }) |k| { + inline for ([_]comptime_int{ 5 * 4, 11 * 4 }) |m| { + out[k * W * 4 + m + 0] = f; + out[k * W * 4 + m + 1] = f; + out[k * W * 4 + m + 2] = f; + out[k * W * 4 + m + 3] = 0; + } + } + + inline for ([_]comptime_int{ 3, 4 }) |k| { + inline for ([_]comptime_int{ 2 * 4, 8 * 4 }) |m| { + out[k * W * 4 + m + 0] = f; + out[k * W * 4 + m + 1] = f; + out[k * W * 4 + m + 2] = f; + out[k * W * 4 + m + 3] = 0; + } + } + + inline for ([_]comptime_int{ 2, 5 }) |k| { + comptime var m = 0; + inline while (m < W * 4) : (m += 4) { + out[k * W * 4 + m + 0] = f; + out[k * W * 4 + m + 1] = f; + out[k * W * 4 + m + 2] = f; + out[k * W * 4 + m + 3] = 0; + } + } + + return out; + } +}; + +// don't confuse with gpu.Texture +const Texture = struct { + const Self = @This(); + + texture: *gpu.Texture, + view: *gpu.TextureView, + sampler: *gpu.Sampler, + bind_group: ?*gpu.BindGroup, + + const DEPTH_FORMAT = .depth32_float; + const FORMAT = .rgba8_unorm; + + fn release(self: *Self) void { + self.texture.release(); + self.view.release(); + self.sampler.release(); + if (self.bind_group) |bind_group| bind_group.release(); + } + + fn fromData(device: *gpu.Device, width: u32, height: u32, comptime T: type, data: []const T) Self { + const extent = gpu.Extent3D{ + .width = width, + .height = height, + }; + + const texture = device.createTexture(&gpu.Texture.Descriptor{ + .size = extent, + .format = FORMAT, + .usage = .{ .copy_dst = true, .texture_binding = true }, + }); + + const view = texture.createView(&gpu.TextureView.Descriptor{ + .format = FORMAT, + .dimension = .dimension_2d, + .array_layer_count = 1, + .mip_level_count = 1, + }); + + const sampler = device.createSampler(&gpu.Sampler.Descriptor{ + .address_mode_u = .repeat, + .address_mode_v = .repeat, + .address_mode_w = .repeat, + .mag_filter = .linear, + .min_filter = .linear, + .mipmap_filter = .linear, + .max_anisotropy = 1, // 1,2,4,8,16 + }); + + core.queue.writeTexture( + &gpu.ImageCopyTexture{ + .texture = texture, + }, + &gpu.Texture.DataLayout{ + .bytes_per_row = 4 * width, + .rows_per_image = height, + }, + &extent, + data, + ); + + const bind_group_layout = Self.bindGroupLayout(device); + const bind_group = device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{ + .layout = bind_group_layout, + .entries = &.{ + gpu.BindGroup.Entry.textureView(0, view), + gpu.BindGroup.Entry.sampler(1, sampler), + }, + })); + bind_group_layout.release(); + + return Self{ + .view = view, + .texture = texture, + .sampler = sampler, + .bind_group = bind_group, + }; + } + + fn depth(device: *gpu.Device, width: u32, height: u32) Self { + const extent = gpu.Extent3D{ + .width = width, + .height = height, + }; + + const texture = device.createTexture(&gpu.Texture.Descriptor{ + .size = extent, + .format = DEPTH_FORMAT, + .usage = .{ + .render_attachment = true, + .texture_binding = true, + }, + }); + + const view = texture.createView(&gpu.TextureView.Descriptor{ + .dimension = .dimension_2d, + .array_layer_count = 1, + .mip_level_count = 1, + }); + + const sampler = device.createSampler(&gpu.Sampler.Descriptor{ + .mag_filter = .linear, + .compare = .less_equal, + }); + + return Self{ + .texture = texture, + .view = view, + .sampler = sampler, + .bind_group = null, // not used + }; + } + + inline fn bindGroupLayout(device: *gpu.Device) *gpu.BindGroupLayout { + const visibility = .{ .fragment = true }; + const Entry = gpu.BindGroupLayout.Entry; + return device.createBindGroupLayout(&gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &.{ + Entry.texture(0, visibility, .float, .dimension_2d, false), + Entry.sampler(1, visibility, .filtering), + }, + })); + } +}; + +const Light = struct { + const Self = @This(); + + uniform: Uniform, + buffer: Buffer, + bind_group: *gpu.BindGroup, + pipeline: *gpu.RenderPipeline, + + const Uniform = extern struct { + position: Vec, + color: Vec, + }; + + fn init() Self { + const device = core.device; + const uniform = Uniform{ + .color = vec3u(1, 1, 1), + .position = vec3u(3, 7, 2), + }; + + const buffer = .{ + .buffer = initBuffer(device, .{ .uniform = true }, &@as([8]f32, @bitCast(uniform))), + .size = @sizeOf(@TypeOf(uniform)), + }; + + const layout = Self.bindGroupLayout(device); + const bind_group = device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{ + .layout = layout, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, buffer.buffer, 0, buffer.size, buffer.size), + }, + })); + layout.release(); + + return Self{ + .buffer = buffer, + .uniform = uniform, + .bind_group = bind_group, + .pipeline = Self.pipeline(), + }; + } + + fn deinit(self: *Self) void { + self.buffer.release(); + self.bind_group.release(); + self.pipeline.release(); + } + + fn update(self: *Self, queue: *gpu.Queue, delta: f32) void { + const old = self.uniform; + const new = Light.Uniform{ + .position = zm.qmul(zm.quatFromAxisAngle(vec3u(0, 1, 0), delta), old.position), + .color = old.color, + }; + queue.writeBuffer(self.buffer.buffer, 0, &[_]Light.Uniform{new}); + self.uniform = new; + } + + inline fn bindGroupLayout(device: *gpu.Device) *gpu.BindGroupLayout { + const visibility = .{ .vertex = true, .fragment = true }; + const Entry = gpu.BindGroupLayout.Entry; + return device.createBindGroupLayout(&gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &.{ + Entry.buffer(0, visibility, .uniform, false, 0), + }, + })); + } + + fn pipeline() *gpu.RenderPipeline { + const device = core.device; + + const camera_layout = Camera.bindGroupLayout(device); + const light_layout = Light.bindGroupLayout(device); + const layout_descriptor = gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &.{ + camera_layout, + light_layout, + }, + }); + defer camera_layout.release(); + defer light_layout.release(); + + const layout = device.createPipelineLayout(&layout_descriptor); + defer layout.release(); + + const shader = core.device.createShaderModuleWGSL("light.wgsl", @embedFile("light.wgsl")); + defer shader.release(); + + const blend = gpu.BlendState{}; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + }; + + const fragment = gpu.FragmentState.init(.{ + .module = shader, + .entry_point = "fs_main", + .targets = &.{color_target}, + }); + + const descriptor = gpu.RenderPipeline.Descriptor{ + .layout = layout, + .fragment = &fragment, + .vertex = gpu.VertexState.init(.{ + .module = shader, + .entry_point = "vs_main", + .buffers = &.{ + Cube.vertexBufferLayout(), + }, + }), + .depth_stencil = &.{ + .format = Texture.DEPTH_FORMAT, + .depth_write_enabled = .true, + .depth_compare = .less, + }, + .primitive = .{ + .cull_mode = .back, + .topology = .triangle_strip, + }, + }; + + return device.createRenderPipeline(&descriptor); + } +}; + +inline fn initBuffer(device: *gpu.Device, usage: gpu.Buffer.UsageFlags, data: anytype) *gpu.Buffer { + std.debug.assert(@typeInfo(@TypeOf(data)) == .Pointer); + const T = std.meta.Elem(@TypeOf(data)); + + var u = usage; + u.copy_dst = true; + const buffer = device.createBuffer(&.{ + .size = @sizeOf(T) * data.len, + .usage = u, + .mapped_at_creation = .true, + }); + + const mapped = buffer.getMappedRange(T, 0, data.len); + @memcpy(mapped.?, data); + buffer.unmap(); + return buffer; +} + +fn vec3i(x: isize, y: isize, z: isize) Vec { + return Vec{ @floatFromInt(x), @floatFromInt(y), @floatFromInt(z), 0.0 }; +} + +fn vec3u(x: usize, y: usize, z: usize) Vec { + return zm.Vec{ @floatFromInt(x), @floatFromInt(y), @floatFromInt(z), 0.0 }; +} + +// todo indside Cube +const Instance = struct { + const Self = @This(); + + position: Vec, + rotation: Mat, + + fn toMat(self: *const Self) Mat { + return zm.mul(self.rotation, zm.translationV(self.position)); + } +}; diff --git a/src/core/examples/sysgpu/image-blur/blur.wgsl b/src/core/examples/sysgpu/image-blur/blur.wgsl new file mode 100644 index 00000000..f97449c1 --- /dev/null +++ b/src/core/examples/sysgpu/image-blur/blur.wgsl @@ -0,0 +1,82 @@ +struct Params { + filterDim : i32, + blockDim : u32, +} + +@group(0) @binding(0) var samp : sampler; +@group(0) @binding(1) var params : Params; +@group(1) @binding(1) var inputTex : texture_2d; +@group(1) @binding(2) var outputTex : texture_storage_2d; + +struct Flip { + value : u32, +} +@group(1) @binding(3) var flip : Flip; + +// This shader blurs the input texture in one direction, depending on whether +// |flip.value| is 0 or 1. +// It does so by running (128 / 4) threads per workgroup to load 128 +// texels into 4 rows of shared memory. Each thread loads a +// 4 x 4 block of texels to take advantage of the texture sampling +// hardware. +// Then, each thread computes the blur result by averaging the adjacent texel values +// in shared memory. +// Because we're operating on a subset of the texture, we cannot compute all of the +// results since not all of the neighbors are available in shared memory. +// Specifically, with 128 x 128 tiles, we can only compute and write out +// square blocks of size 128 - (filterSize - 1). We compute the number of blocks +// needed in Javascript and dispatch that amount. + +var tile : array, 128>, 4>; + +@compute @workgroup_size(32, 1, 1) +fn main( + @builtin(workgroup_id) WorkGroupID : vec3, + @builtin(local_invocation_id) LocalInvocationID : vec3 +) { + // TODO - mixed vector arithmetic (vec2 and vec2) + let filterOffset = (params.filterDim - 1) / 2; + let dims = vec2(textureDimensions(inputTex, 0)); + let baseIndex = vec2(WorkGroupID.xy * vec2(params.blockDim, 4) + + LocalInvocationID.xy * vec2(4, 1)) + - vec2(filterOffset, 0); + + for (var r = 0; r < 4; r++) { + for (var c = 0; c < 4; c++) { + var loadIndex = baseIndex + vec2(c, r); + if (flip.value != 0u) { + loadIndex = loadIndex.yx; + } + + tile[r][4 * LocalInvocationID.x + u32(c)] = textureSampleLevel( + inputTex, + samp, + (vec2(loadIndex) + vec2(0.25, 0.25)) / vec2(dims), + 0.0 + ).rgb; + } + } + + workgroupBarrier(); + + for (var r = 0; r < 4; r++) { + for (var c = 0; c < 4; c++) { + var writeIndex = baseIndex + vec2(c, r); + if (flip.value != 0) { + writeIndex = writeIndex.yx; + } + + let center = u32(4 * LocalInvocationID.x) + c; + if (center >= filterOffset && + center < 128 - filterOffset && + all(writeIndex < dims)) { + var acc = vec3(0.0, 0.0, 0.0); + for (var f = 0; f < params.filterDim; f++) { + var i = center + f - filterOffset; + acc = acc + (1.0 / f32(params.filterDim)) * tile[r][i]; + } + textureStore(outputTex, writeIndex, vec4(acc, 1.0)); + } + } + } +} diff --git a/src/core/examples/sysgpu/image-blur/fullscreen_textured_quad.wgsl b/src/core/examples/sysgpu/image-blur/fullscreen_textured_quad.wgsl new file mode 100644 index 00000000..61c461c0 --- /dev/null +++ b/src/core/examples/sysgpu/image-blur/fullscreen_textured_quad.wgsl @@ -0,0 +1,38 @@ +@group(0) @binding(0) var mySampler : sampler; +@group(0) @binding(1) var myTexture : texture_2d; + +struct VertexOutput { + @builtin(position) Position : vec4, + @location(0) fragUV : vec2, +} + +@vertex +fn vert_main(@builtin(vertex_index) VertexIndex : u32) -> VertexOutput { + var pos = array, 6>( + vec2( 1.0, 1.0), + vec2( 1.0, -1.0), + vec2(-1.0, -1.0), + vec2( 1.0, 1.0), + vec2(-1.0, -1.0), + vec2(-1.0, 1.0) + ); + + var uv = array, 6>( + vec2(1.0, 0.0), + vec2(1.0, 1.0), + vec2(0.0, 1.0), + vec2(1.0, 0.0), + vec2(0.0, 1.0), + vec2(0.0, 0.0) + ); + + var output : VertexOutput; + output.Position = vec4(pos[VertexIndex], 0.0, 1.0); + output.fragUV = uv[VertexIndex]; + return output; +} + +@fragment +fn frag_main(@location(0) fragUV : vec2) -> @location(0) vec4 { + return textureSample(myTexture, mySampler, fragUV); +} diff --git a/src/core/examples/sysgpu/image-blur/main.zig b/src/core/examples/sysgpu/image-blur/main.zig new file mode 100644 index 00000000..cb9eedfd --- /dev/null +++ b/src/core/examples/sysgpu/image-blur/main.zig @@ -0,0 +1,331 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const zigimg = @import("zigimg"); +const assets = @import("assets"); + +title_timer: core.Timer, +blur_pipeline: *gpu.ComputePipeline, +fullscreen_quad_pipeline: *gpu.RenderPipeline, +cube_texture: *gpu.Texture, +textures: [2]*gpu.Texture, +blur_params_buffer: *gpu.Buffer, +compute_constants: *gpu.BindGroup, +compute_bind_group_0: *gpu.BindGroup, +compute_bind_group_1: *gpu.BindGroup, +compute_bind_group_2: *gpu.BindGroup, +show_result_bind_group: *gpu.BindGroup, +img_size: gpu.Extent3D, + +pub const App = @This(); + +pub const mach_core_options = core.ComptimeOptions{ + .use_wgpu = false, + .use_sysgpu = true, +}; + +// Constants from the blur.wgsl shader +const tile_dimension: u32 = 128; +const batch: [2]u32 = .{ 4, 4 }; + +// Currently hardcoded +const filter_size: u32 = 15; +const iterations: u32 = 2; +var block_dimension: u32 = tile_dimension - (filter_size - 1); +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +pub fn init(app: *App) !void { + try core.init(.{}); + const allocator = gpa.allocator(); + + const queue = core.queue; + + const blur_shader_module = core.device.createShaderModuleWGSL("blur.wgsl", @embedFile("blur.wgsl")); + + const blur_pipeline_descriptor = gpu.ComputePipeline.Descriptor{ + .compute = gpu.ProgrammableStageDescriptor{ + .module = blur_shader_module, + .entry_point = "main", + }, + }; + + const blur_pipeline = core.device.createComputePipeline(&blur_pipeline_descriptor); + blur_shader_module.release(); + + const fullscreen_quad_vs_module = core.device.createShaderModuleWGSL( + "fullscreen_textured_quad.wgsl", + @embedFile("fullscreen_textured_quad.wgsl"), + ); + + const fullscreen_quad_fs_module = core.device.createShaderModuleWGSL( + "fullscreen_textured_quad.wgsl", + @embedFile("fullscreen_textured_quad.wgsl"), + ); + + const blend = gpu.BlendState{}; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + + const fragment_state = gpu.FragmentState.init(.{ + .module = fullscreen_quad_fs_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + + const fullscreen_quad_pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment_state, + .vertex = .{ + .module = fullscreen_quad_vs_module, + .entry_point = "vert_main", + }, + }; + + const fullscreen_quad_pipeline = core.device.createRenderPipeline(&fullscreen_quad_pipeline_descriptor); + fullscreen_quad_vs_module.release(); + fullscreen_quad_fs_module.release(); + + const sampler = core.device.createSampler(&.{ + .mag_filter = .linear, + .min_filter = .linear, + }); + + var img = try zigimg.Image.fromMemory(allocator, assets.gotta_go_fast_png); + defer img.deinit(); + + const img_size = gpu.Extent3D{ .width = @as(u32, @intCast(img.width)), .height = @as(u32, @intCast(img.height)) }; + + const cube_texture = core.device.createTexture(&.{ + .size = img_size, + .format = .rgba8_unorm, + .usage = .{ + .texture_binding = true, + .copy_dst = true, + .render_attachment = true, + }, + }); + + const data_layout = gpu.Texture.DataLayout{ + .bytes_per_row = @as(u32, @intCast(img.width * 4)), + .rows_per_image = @as(u32, @intCast(img.height)), + }; + + switch (img.pixels) { + .rgba32 => |pixels| queue.writeTexture(&.{ .texture = cube_texture }, &data_layout, &img_size, pixels), + .rgb24 => |pixels| { + const data = try rgb24ToRgba32(allocator, pixels); + defer data.deinit(allocator); + queue.writeTexture(&.{ .texture = cube_texture }, &data_layout, &img_size, data.rgba32); + }, + else => @panic("unsupported image color format"), + } + + var textures: [2]*gpu.Texture = undefined; + for (textures, 0..) |_, i| { + textures[i] = core.device.createTexture(&.{ + .size = img_size, + .format = .rgba8_unorm, + .usage = .{ + .storage_binding = true, + .texture_binding = true, + .copy_dst = true, + }, + }); + } + + // the shader blurs the input texture in one direction, + // depending on whether flip value is 0 or 1 + var flip: [2]*gpu.Buffer = undefined; + for (flip, 0..) |_, i| { + const buffer = core.device.createBuffer(&.{ + .usage = .{ .uniform = true }, + .size = @sizeOf(u32), + .mapped_at_creation = .true, + }); + + const buffer_mapped = buffer.getMappedRange(u32, 0, 1); + buffer_mapped.?[0] = @as(u32, @intCast(i)); + buffer.unmap(); + + flip[i] = buffer; + } + + const blur_params_buffer = core.device.createBuffer(&.{ + .size = 8, + .usage = .{ .copy_dst = true, .uniform = true }, + }); + + const blur_bind_group_layout0 = blur_pipeline.getBindGroupLayout(0); + const blur_bind_group_layout1 = blur_pipeline.getBindGroupLayout(1); + const fullscreen_bind_group_layout = fullscreen_quad_pipeline.getBindGroupLayout(0); + const cube_texture_view = cube_texture.createView(&gpu.TextureView.Descriptor{}); + const texture0_view = textures[0].createView(&gpu.TextureView.Descriptor{}); + const texture1_view = textures[1].createView(&gpu.TextureView.Descriptor{}); + + const compute_constants = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{ + .layout = blur_bind_group_layout0, + .entries = &.{ + gpu.BindGroup.Entry.sampler(0, sampler), + gpu.BindGroup.Entry.buffer(1, blur_params_buffer, 0, 8, 8), + }, + })); + + const compute_bind_group_0 = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{ + .layout = blur_bind_group_layout1, + .entries = &.{ + gpu.BindGroup.Entry.textureView(1, cube_texture_view), + gpu.BindGroup.Entry.textureView(2, texture0_view), + gpu.BindGroup.Entry.buffer(3, flip[0], 0, 4, 4), + }, + })); + + const compute_bind_group_1 = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{ + .layout = blur_bind_group_layout1, + .entries = &.{ + gpu.BindGroup.Entry.textureView(1, texture0_view), + gpu.BindGroup.Entry.textureView(2, texture1_view), + gpu.BindGroup.Entry.buffer(3, flip[1], 0, 4, 4), + }, + })); + + const compute_bind_group_2 = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{ + .layout = blur_bind_group_layout1, + .entries = &.{ + gpu.BindGroup.Entry.textureView(1, texture1_view), + gpu.BindGroup.Entry.textureView(2, texture0_view), + gpu.BindGroup.Entry.buffer(3, flip[0], 0, 4, 4), + }, + })); + + const show_result_bind_group = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{ + .layout = fullscreen_bind_group_layout, + .entries = &.{ + gpu.BindGroup.Entry.sampler(0, sampler), + gpu.BindGroup.Entry.textureView(1, texture1_view), + }, + })); + + blur_bind_group_layout0.release(); + blur_bind_group_layout1.release(); + fullscreen_bind_group_layout.release(); + sampler.release(); + flip[0].release(); + flip[1].release(); + cube_texture_view.release(); + texture0_view.release(); + texture1_view.release(); + + const blur_params_buffer_data = [_]u32{ filter_size, block_dimension }; + queue.writeBuffer(blur_params_buffer, 0, &blur_params_buffer_data); + + app.title_timer = try core.Timer.start(); + app.blur_pipeline = blur_pipeline; + app.fullscreen_quad_pipeline = fullscreen_quad_pipeline; + app.cube_texture = cube_texture; + app.textures = textures; + app.blur_params_buffer = blur_params_buffer; + app.compute_constants = compute_constants; + app.compute_bind_group_0 = compute_bind_group_0; + app.compute_bind_group_1 = compute_bind_group_1; + app.compute_bind_group_2 = compute_bind_group_2; + app.show_result_bind_group = show_result_bind_group; + app.img_size = img_size; +} + +pub fn deinit(app: *App) void { + defer _ = gpa.deinit(); + defer core.deinit(); + + app.blur_pipeline.release(); + app.fullscreen_quad_pipeline.release(); + app.cube_texture.release(); + app.textures[0].release(); + app.textures[1].release(); + app.blur_params_buffer.release(); + app.compute_constants.release(); + app.compute_bind_group_0.release(); + app.compute_bind_group_1.release(); + app.compute_bind_group_2.release(); + app.show_result_bind_group.release(); +} + +pub fn update(app: *App) !bool { + var iter = core.pollEvents(); + while (iter.next()) |event| { + if (event == .close) return true; + } + + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const encoder = core.device.createCommandEncoder(null); + + const compute_pass = encoder.beginComputePass(null); + compute_pass.setPipeline(app.blur_pipeline); + compute_pass.setBindGroup(0, app.compute_constants, &.{}); + + const width: u32 = @as(u32, @intCast(app.img_size.width)); + const height: u32 = @as(u32, @intCast(app.img_size.height)); + compute_pass.setBindGroup(1, app.compute_bind_group_0, &.{}); + compute_pass.dispatchWorkgroups(try std.math.divCeil(u32, width, block_dimension), try std.math.divCeil(u32, height, batch[1]), 1); + + compute_pass.setBindGroup(1, app.compute_bind_group_1, &.{}); + compute_pass.dispatchWorkgroups(try std.math.divCeil(u32, height, block_dimension), try std.math.divCeil(u32, width, batch[1]), 1); + + var i: u32 = 0; + while (i < iterations - 1) : (i += 1) { + compute_pass.setBindGroup(1, app.compute_bind_group_2, &.{}); + compute_pass.dispatchWorkgroups(try std.math.divCeil(u32, width, block_dimension), try std.math.divCeil(u32, height, batch[1]), 1); + + compute_pass.setBindGroup(1, app.compute_bind_group_1, &.{}); + compute_pass.dispatchWorkgroups(try std.math.divCeil(u32, height, block_dimension), try std.math.divCeil(u32, width, batch[1]), 1); + } + compute_pass.end(); + compute_pass.release(); + + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = std.mem.zeroes(gpu.Color), + .load_op = .clear, + .store_op = .store, + }; + + const render_pass_descriptor = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + }); + + const render_pass = encoder.beginRenderPass(&render_pass_descriptor); + render_pass.setPipeline(app.fullscreen_quad_pipeline); + render_pass.setBindGroup(0, app.show_result_bind_group, &.{}); + render_pass.draw(6, 1, 0, 0); + render_pass.end(); + render_pass.release(); + + var command = encoder.finish(null); + encoder.release(); + const queue = core.queue; + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Image Blur [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} + +fn rgb24ToRgba32(allocator: std.mem.Allocator, in: []zigimg.color.Rgb24) !zigimg.color.PixelStorage { + const out = try zigimg.color.PixelStorage.init(allocator, .rgba32, in.len); + var i: usize = 0; + while (i < in.len) : (i += 1) { + out.rgba32[i] = zigimg.color.Rgba32{ .r = in[i].r, .g = in[i].g, .b = in[i].b, .a = 255 }; + } + return out; +} diff --git a/src/core/examples/sysgpu/image/fullscreen_textured_quad.wgsl b/src/core/examples/sysgpu/image/fullscreen_textured_quad.wgsl new file mode 100644 index 00000000..3238e6a4 --- /dev/null +++ b/src/core/examples/sysgpu/image/fullscreen_textured_quad.wgsl @@ -0,0 +1,39 @@ +@group(0) @binding(0) var mySampler : sampler; +@group(0) @binding(1) var myTexture : texture_2d; + +struct VertexOutput { + @builtin(position) Position : vec4, + @location(0) fragUV : vec2, +} + +@vertex +fn vert_main(@builtin(vertex_index) VertexIndex : u32) -> VertexOutput { + // Draw a fullscreen quad using two triangles, with UV coordinates (normalized pixel coordinates) + // that would have the full texture be displayed. + var pos = array, 6>( + vec2( 1.0, 1.0), // right, top + vec2( 1.0, -1.0), // right, bottom + vec2(-1.0, -1.0), // left, bottom + vec2( 1.0, 1.0), // right, top + vec2(-1.0, -1.0), // left, bottom + vec2(-1.0, 1.0) // left, top + ); + var uv = array, 6>( + vec2(1.0, 0.0), + vec2(1.0, 1.0), + vec2(0.0, 1.0), + vec2(1.0, 0.0), + vec2(0.0, 1.0), + vec2(0.0, 0.0) + ); + + var output : VertexOutput; + output.Position = vec4(pos[VertexIndex], 0.0, 1.0); + output.fragUV = uv[VertexIndex]; + return output; +} + +@fragment +fn frag_main(@location(0) fragUV : vec2) -> @location(0) vec4 { + return textureSample(myTexture, mySampler, fragUV); +} diff --git a/src/core/examples/sysgpu/image/main.zig b/src/core/examples/sysgpu/image/main.zig new file mode 100644 index 00000000..b605e677 --- /dev/null +++ b/src/core/examples/sysgpu/image/main.zig @@ -0,0 +1,189 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const zigimg = @import("zigimg"); +const assets = @import("assets"); + +pub const mach_core_options = core.ComptimeOptions{ + .use_wgpu = false, + .use_sysgpu = true, +}; + +title_timer: core.Timer, +pipeline: *gpu.RenderPipeline, +texture: *gpu.Texture, +bind_group: *gpu.BindGroup, +img_size: gpu.Extent3D, + +pub const App = @This(); + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +pub fn init(app: *App) !void { + try core.init(.{}); + const allocator = gpa.allocator(); + + // Load our shader that will render a fullscreen textured quad using two triangles, needed to + // get the image on screen. + const fullscreen_quad_vs_module = core.device.createShaderModuleWGSL( + "fullscreen_textured_quad.wgsl", + @embedFile("fullscreen_textured_quad.wgsl"), + ); + defer fullscreen_quad_vs_module.release(); + const fullscreen_quad_fs_module = core.device.createShaderModuleWGSL( + "fullscreen_textured_quad.wgsl", + @embedFile("fullscreen_textured_quad.wgsl"), + ); + defer fullscreen_quad_fs_module.release(); + + // Create our render pipeline + const blend = gpu.BlendState{}; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment_state = gpu.FragmentState.init(.{ + .module = fullscreen_quad_fs_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment_state, + .vertex = .{ + .module = fullscreen_quad_vs_module, + .entry_point = "vert_main", + }, + }; + const pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + + // Create a texture sampler. This determines what happens when the texture doesn't match the + // dimensions of the screen it's being displayed on. If the image needs to be magnified or + // minified to fit, it can be linearly interpolated (i.e. 'blurred', .linear) or the nearest + // pixel may be used (i.e. 'pixelated', .nearest) + const sampler = core.device.createSampler(&.{ + .mag_filter = .linear, + .min_filter = .linear, + }); + defer sampler.release(); + + // Load the pixels of the image + var img = try zigimg.Image.fromMemory(allocator, assets.gotta_go_fast_png); + defer img.deinit(); + const img_size = gpu.Extent3D{ .width = @as(u32, @intCast(img.width)), .height = @as(u32, @intCast(img.height)) }; + + // Create a texture + const texture = core.device.createTexture(&.{ + .size = img_size, + .format = .rgba8_unorm, + .usage = .{ + .texture_binding = true, + .copy_dst = true, + .render_attachment = true, + }, + }); + + // Upload the pixels (from the CPU) to the GPU. You could e.g. do this once per frame if you + // wanted the image to be updated dynamically. + const data_layout = gpu.Texture.DataLayout{ + .bytes_per_row = @as(u32, @intCast(img.width * 4)), + .rows_per_image = @as(u32, @intCast(img.height)), + }; + switch (img.pixels) { + .rgba32 => |pixels| core.queue.writeTexture(&.{ .texture = texture }, &data_layout, &img_size, pixels), + .rgb24 => |pixels| { + const data = try rgb24ToRgba32(allocator, pixels); + defer data.deinit(allocator); + core.queue.writeTexture(&.{ .texture = texture }, &data_layout, &img_size, data.rgba32); + }, + else => @panic("unsupported image color format"), + } + + // Describe which data we will pass to our shader (GPU program) + const bind_group_layout = pipeline.getBindGroupLayout(0); + defer bind_group_layout.release(); + const texture_view = texture.createView(&gpu.TextureView.Descriptor{}); + defer texture_view.release(); + const bind_group = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{ + .layout = bind_group_layout, + .entries = &.{ + gpu.BindGroup.Entry.sampler(0, sampler), + gpu.BindGroup.Entry.textureView(1, texture_view), + }, + })); + + app.* = .{ + .title_timer = try core.Timer.start(), + .pipeline = pipeline, + .texture = texture, + .bind_group = bind_group, + .img_size = img_size, + }; +} + +pub fn deinit(app: *App) void { + defer _ = gpa.deinit(); + defer core.deinit(); + app.pipeline.release(); + app.texture.release(); + app.bind_group.release(); +} + +pub fn update(app: *App) !bool { + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + defer back_buffer_view.release(); + + // Poll for events (keyboard input, etc.) + var iter = core.pollEvents(); + while (iter.next()) |event| { + if (event == .close) return true; + } + + const encoder = core.device.createCommandEncoder(null); + defer encoder.release(); + + // Begin our render pass by clearing the pixels that were on the screen from the previous frame. + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = std.mem.zeroes(gpu.Color), + .load_op = .clear, + .store_op = .store, + }; + const render_pass_descriptor = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + }); + const render_pass = encoder.beginRenderPass(&render_pass_descriptor); + defer render_pass.release(); + + // Render using our pipeline + render_pass.setPipeline(app.pipeline); + render_pass.setBindGroup(0, app.bind_group, &.{}); + render_pass.draw(6, 1, 0, 0); // Tell the GPU to draw 6 vertices, one object + render_pass.end(); + + // Submit all the commands to the GPU and render the frame. + var command = encoder.finish(null); + defer command.release(); + core.queue.submit(&[_]*gpu.CommandBuffer{command}); + core.swap_chain.present(); + + // update the window title every second to have the FPS + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Image [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} + +fn rgb24ToRgba32(allocator: std.mem.Allocator, in: []zigimg.color.Rgb24) !zigimg.color.PixelStorage { + const out = try zigimg.color.PixelStorage.init(allocator, .rgba32, in.len); + var i: usize = 0; + while (i < in.len) : (i += 1) { + out.rgba32[i] = zigimg.color.Rgba32{ .r = in[i].r, .g = in[i].g, .b = in[i].b, .a = 255 }; + } + return out; +} diff --git a/src/core/examples/sysgpu/instanced-cube/cube_mesh.zig b/src/core/examples/sysgpu/instanced-cube/cube_mesh.zig new file mode 100644 index 00000000..f26c75ac --- /dev/null +++ b/src/core/examples/sysgpu/instanced-cube/cube_mesh.zig @@ -0,0 +1,49 @@ +pub const Vertex = extern struct { + pos: @Vector(4, f32), + col: @Vector(4, f32), + uv: @Vector(2, f32), +}; + +pub const vertices = [_]Vertex{ + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, +}; diff --git a/src/core/examples/sysgpu/instanced-cube/main.zig b/src/core/examples/sysgpu/instanced-cube/main.zig new file mode 100644 index 00000000..8fbfe7f4 --- /dev/null +++ b/src/core/examples/sysgpu/instanced-cube/main.zig @@ -0,0 +1,210 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const zm = @import("zmath"); +const Vertex = @import("cube_mesh.zig").Vertex; +const vertices = @import("cube_mesh.zig").vertices; + +const UniformBufferObject = struct { + mat: zm.Mat, +}; + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +title_timer: core.Timer, + +timer: core.Timer, +pipeline: *gpu.RenderPipeline, +vertex_buffer: *gpu.Buffer, +uniform_buffer: *gpu.Buffer, +bind_group: *gpu.BindGroup, + +pub const App = @This(); + +pub const mach_core_options = core.ComptimeOptions{ + .use_wgpu = false, + .use_sysgpu = true, +}; + +pub fn init(app: *App) !void { + try core.init(.{}); + app.timer = try core.Timer.start(); + + const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); + + const vertex_attributes = [_]gpu.VertexAttribute{ + .{ .format = .float32x4, .offset = @offsetOf(Vertex, "pos"), .shader_location = 0 }, + .{ .format = .float32x2, .offset = @offsetOf(Vertex, "uv"), .shader_location = 1 }, + }; + const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{ + .array_stride = @sizeOf(Vertex), + .step_mode = .vertex, + .attributes = &vertex_attributes, + }); + + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + + const bgle = gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, true, 0); + const bgl = core.device.createBindGroupLayout( + &gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &.{bgle}, + }), + ); + + const bind_group_layouts = [_]*gpu.BindGroupLayout{bgl}; + const pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &bind_group_layouts, + })); + + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + .layout = pipeline_layout, + .vertex = gpu.VertexState.init(.{ + .module = shader_module, + .entry_point = "vertex_main", + .buffers = &.{vertex_buffer_layout}, + }), + .primitive = .{ + .cull_mode = .back, + }, + }; + + const vertex_buffer = core.device.createBuffer(&.{ + .usage = .{ .vertex = true }, + .size = @sizeOf(Vertex) * vertices.len, + .mapped_at_creation = .true, + }); + const vertex_mapped = vertex_buffer.getMappedRange(Vertex, 0, vertices.len); + @memcpy(vertex_mapped.?, vertices[0..]); + vertex_buffer.unmap(); + + const x_count = 4; + const y_count = 4; + const num_instances = x_count * y_count; + + const uniform_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = @sizeOf(UniformBufferObject) * num_instances, + .mapped_at_creation = .false, + }); + const bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bgl, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, uniform_buffer, 0, @sizeOf(UniformBufferObject) * num_instances, @sizeOf(UniformBufferObject)), + }, + }), + ); + + app.title_timer = try core.Timer.start(); + app.pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + app.vertex_buffer = vertex_buffer; + app.uniform_buffer = uniform_buffer; + app.bind_group = bind_group; + + shader_module.release(); + pipeline_layout.release(); + bgl.release(); +} + +pub fn deinit(app: *App) void { + defer _ = gpa.deinit(); + defer core.deinit(); + + app.pipeline.release(); + app.vertex_buffer.release(); + app.bind_group.release(); + app.uniform_buffer.release(); +} + +pub fn update(app: *App) !bool { + var iter = core.pollEvents(); + while (iter.next()) |event| { + switch (event) { + .key_press => |ev| { + if (ev.key == .space) return true; + }, + .close => return true, + else => {}, + } + } + + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = std.mem.zeroes(gpu.Color), + .load_op = .clear, + .store_op = .store, + }; + + const encoder = core.device.createCommandEncoder(null); + const render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + }); + + { + const proj = zm.perspectiveFovRh( + (std.math.pi / 3.0), + @as(f32, @floatFromInt(core.descriptor.width)) / @as(f32, @floatFromInt(core.descriptor.height)), + 10, + 30, + ); + + var ubos: [16]UniformBufferObject = undefined; + const time = app.timer.read(); + const step: f32 = 4.0; + var m: u8 = 0; + var x: u8 = 0; + while (x < 4) : (x += 1) { + var y: u8 = 0; + while (y < 4) : (y += 1) { + const trans = zm.translation(step * (@as(f32, @floatFromInt(x)) - 2.0 + 0.5), step * (@as(f32, @floatFromInt(y)) - 2.0 + 0.5), -20); + const localTime = time + @as(f32, @floatFromInt(m)) * 0.5; + const model = zm.mul(zm.mul(zm.mul(zm.rotationX(localTime * (std.math.pi / 2.1)), zm.rotationY(localTime * (std.math.pi / 0.9))), zm.rotationZ(localTime * (std.math.pi / 1.3))), trans); + const mvp = zm.mul(model, proj); + const ubo = UniformBufferObject{ + .mat = mvp, + }; + ubos[m] = ubo; + m += 1; + } + } + encoder.writeBuffer(app.uniform_buffer, 0, &ubos); + } + + const pass = encoder.beginRenderPass(&render_pass_info); + pass.setPipeline(app.pipeline); + pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len); + pass.setBindGroup(0, app.bind_group, &.{0}); + pass.draw(vertices.len, 16, 0, 0); + pass.end(); + pass.release(); + + var command = encoder.finish(null); + encoder.release(); + + const queue = core.queue; + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Instanced Cube [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} diff --git a/src/core/examples/sysgpu/instanced-cube/shader.wgsl b/src/core/examples/sysgpu/instanced-cube/shader.wgsl new file mode 100644 index 00000000..1e279d8d --- /dev/null +++ b/src/core/examples/sysgpu/instanced-cube/shader.wgsl @@ -0,0 +1,25 @@ +@binding(0) @group(0) var ubos : array, 16>; + +struct VertexOutput { + @builtin(position) position_clip : vec4, + @location(0) fragUV : vec2, + @location(1) fragPosition: vec4, +}; + +@vertex +fn vertex_main(@builtin(instance_index) instanceIdx : u32, + @location(0) position : vec4, + @location(1) uv : vec2) -> VertexOutput { + var output : VertexOutput; + output.position_clip = ubos[instanceIdx] * position; + output.fragUV = uv; + output.fragPosition = 0.5 * (position + vec4(1.0, 1.0, 1.0, 1.0)); + return output; +} + +@fragment fn frag_main( + @location(0) fragUV: vec2, + @location(1) fragPosition: vec4 +) -> @location(0) vec4 { + return fragPosition; +} \ No newline at end of file diff --git a/src/core/examples/sysgpu/map-async/main.wgsl b/src/core/examples/sysgpu/map-async/main.wgsl new file mode 100644 index 00000000..a4270992 --- /dev/null +++ b/src/core/examples/sysgpu/map-async/main.wgsl @@ -0,0 +1,16 @@ +@group(0) @binding(0) var output: array; + +@compute @workgroup_size(64, 1, 1) +fn main( + @builtin(global_invocation_id) + global_id : vec3, + + @builtin(local_invocation_id) + local_id : vec3, +) { + if (global_id.x >= arrayLength(&output)) { + return; + } + output[global_id.x] = + f32(global_id.x) * 1000. + f32(local_id.x); +} diff --git a/src/core/examples/sysgpu/map-async/main.zig b/src/core/examples/sysgpu/map-async/main.zig new file mode 100644 index 00000000..7da9e212 --- /dev/null +++ b/src/core/examples/sysgpu/map-async/main.zig @@ -0,0 +1,106 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; + +pub const App = @This(); + +pub const mach_core_options = core.ComptimeOptions{ + .use_wgpu = false, + .use_sysgpu = true, +}; + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +const workgroup_size = 64; +const buffer_size = 1000; + +pub fn init(app: *App) !void { + try core.init(.{}); + app.* = .{}; + + const output = core.device.createBuffer(&.{ + .usage = .{ .storage = true, .copy_src = true }, + .size = buffer_size * @sizeOf(f32), + .mapped_at_creation = .false, + }); + defer output.release(); + + const staging = core.device.createBuffer(&.{ + .usage = .{ .map_read = true, .copy_dst = true }, + .size = buffer_size * @sizeOf(f32), + .mapped_at_creation = .false, + }); + defer staging.release(); + + const compute_module = core.device.createShaderModuleWGSL("main.wgsl", @embedFile("main.wgsl")); + + const compute_pipeline = core.device.createComputePipeline(&gpu.ComputePipeline.Descriptor{ .compute = gpu.ProgrammableStageDescriptor{ + .module = compute_module, + .entry_point = "main", + } }); + defer compute_pipeline.release(); + + const layout = compute_pipeline.getBindGroupLayout(0); + defer layout.release(); + + const compute_bind_group = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{ + .layout = layout, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, output, 0, buffer_size * @sizeOf(f32), @sizeOf(f32)), + }, + })); + defer compute_bind_group.release(); + + compute_module.release(); + + const encoder = core.device.createCommandEncoder(null); + + const compute_pass = encoder.beginComputePass(null); + compute_pass.setPipeline(compute_pipeline); + compute_pass.setBindGroup(0, compute_bind_group, &.{}); + compute_pass.dispatchWorkgroups(try std.math.divCeil(u32, buffer_size, workgroup_size), 1, 1); + compute_pass.end(); + compute_pass.release(); + + encoder.copyBufferToBuffer(output, 0, staging, 0, buffer_size * @sizeOf(f32)); + + var command = encoder.finish(null); + encoder.release(); + + var response: gpu.Buffer.MapAsyncStatus = undefined; + const callback = (struct { + pub inline fn callback(ctx: *gpu.Buffer.MapAsyncStatus, status: gpu.Buffer.MapAsyncStatus) void { + ctx.* = status; + } + }).callback; + + var queue = core.queue; + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + + staging.mapAsync(.{ .read = true }, 0, buffer_size * @sizeOf(f32), &response, callback); + while (true) { + if (response == gpu.Buffer.MapAsyncStatus.success) { + break; + } else { + core.device.tick(); + } + } + + const staging_mapped = staging.getConstMappedRange(f32, 0, buffer_size); + for (staging_mapped.?) |v| { + std.debug.print("{d} ", .{v}); + } + std.debug.print("\n", .{}); + staging.unmap(); +} + +pub fn deinit(app: *App) void { + _ = app; + defer _ = gpa.deinit(); + core.deinit(); +} + +pub fn update(_: *App) !bool { + return true; +} diff --git a/src/core/examples/sysgpu/pbr-basic/main.zig b/src/core/examples/sysgpu/pbr-basic/main.zig new file mode 100644 index 00000000..024b5928 --- /dev/null +++ b/src/core/examples/sysgpu/pbr-basic/main.zig @@ -0,0 +1,931 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const m3d = @import("model3d"); +const zm = @import("zmath"); +const assets = @import("assets"); +const VertexWriter = @import("vertex_writer.zig").VertexWriter; + +pub const App = @This(); + +pub const mach_core_options = core.ComptimeOptions{ + .use_wgpu = false, + .use_sysgpu = true, +}; + +const Vec4 = [4]f32; +const Vec3 = [3]f32; +const Vec2 = [2]f32; +const Mat4 = [4]Vec4; + +fn Dimensions2D(comptime T: type) type { + return struct { + width: T, + height: T, + }; +} + +const Vertex = extern struct { + position: Vec3, + normal: Vec3, +}; + +const Model = struct { + vertex_count: u32, + index_count: u32, + vertex_buffer: *gpu.Buffer, + index_buffer: *gpu.Buffer, +}; + +const Material = struct { + const Params = extern struct { + roughness: f32, + metallic: f32, + color: Vec3, + }; + + name: []const u8, + params: Params, +}; + +const PressedKeys = packed struct(u16) { + right: bool = false, + left: bool = false, + up: bool = false, + down: bool = false, + padding: u12 = undefined, + + pub inline fn areKeysPressed(self: @This()) bool { + return (self.up or self.down or self.left or self.right); + } + + pub inline fn clear(self: *@This()) void { + self.right = false; + self.left = false; + self.up = false; + self.down = false; + } +}; + +const Camera = struct { + const Matrices = struct { + perspective: Mat4 = [1]Vec4{[1]f32{0.0} ** 4} ** 4, + view: Mat4 = [1]Vec4{[1]f32{0.0} ** 4} ** 4, + }; + + rotation: Vec3 = .{ 0.0, 0.0, 0.0 }, + position: Vec3 = .{ 0.0, 0.0, 0.0 }, + view_position: Vec4 = .{ 0.0, 0.0, 0.0, 0.0 }, + fov: f32 = 0.0, + znear: f32 = 0.0, + zfar: f32 = 0.0, + rotation_speed: f32 = 0.0, + movement_speed: f32 = 0.0, + updated: bool = false, + matrices: Matrices = .{}, + + pub fn calculateMovement(self: *@This(), pressed_keys: PressedKeys) void { + std.debug.assert(pressed_keys.areKeysPressed()); + const rotation_radians = Vec3{ + toRadians(self.rotation[0]), + toRadians(self.rotation[1]), + toRadians(self.rotation[2]), + }; + var camera_front = zm.Vec{ -zm.cos(rotation_radians[0]) * zm.sin(rotation_radians[1]), zm.sin(rotation_radians[0]), zm.cos(rotation_radians[0]) * zm.cos(rotation_radians[1]), 0 }; + camera_front = zm.normalize3(camera_front); + if (pressed_keys.up) { + camera_front[0] *= self.movement_speed; + camera_front[1] *= self.movement_speed; + camera_front[2] *= self.movement_speed; + self.position = Vec3{ + self.position[0] + camera_front[0], + self.position[1] + camera_front[1], + self.position[2] + camera_front[2], + }; + } + if (pressed_keys.down) { + camera_front[0] *= self.movement_speed; + camera_front[1] *= self.movement_speed; + camera_front[2] *= self.movement_speed; + self.position = Vec3{ + self.position[0] - camera_front[0], + self.position[1] - camera_front[1], + self.position[2] - camera_front[2], + }; + } + if (pressed_keys.right) { + camera_front = zm.cross3(.{ 0.0, 1.0, 0.0, 0.0 }, camera_front); + camera_front = zm.normalize3(camera_front); + camera_front[0] *= self.movement_speed; + camera_front[1] *= self.movement_speed; + camera_front[2] *= self.movement_speed; + self.position = Vec3{ + self.position[0] - camera_front[0], + self.position[1] - camera_front[1], + self.position[2] - camera_front[2], + }; + } + if (pressed_keys.left) { + camera_front = zm.cross3(.{ 0.0, 1.0, 0.0, 0.0 }, camera_front); + camera_front = zm.normalize3(camera_front); + camera_front[0] *= self.movement_speed; + camera_front[1] *= self.movement_speed; + camera_front[2] *= self.movement_speed; + self.position = Vec3{ + self.position[0] + camera_front[0], + self.position[1] + camera_front[1], + self.position[2] + camera_front[2], + }; + } + self.updateViewMatrix(); + } + + fn updateViewMatrix(self: *@This()) void { + const rotation_x = zm.rotationX(toRadians(self.rotation[2])); + const rotation_y = zm.rotationY(toRadians(self.rotation[1])); + const rotation_z = zm.rotationZ(toRadians(self.rotation[0])); + const rotation_matrix = zm.mul(rotation_z, zm.mul(rotation_x, rotation_y)); + + const translation_matrix: zm.Mat = zm.translationV(.{ + self.position[0], + self.position[1], + self.position[2], + 0, + }); + const view = zm.mul(translation_matrix, rotation_matrix); + self.matrices.view[0] = view[0]; + self.matrices.view[1] = view[1]; + self.matrices.view[2] = view[2]; + self.matrices.view[3] = view[3]; + self.view_position = .{ + -self.position[0], + self.position[1], + -self.position[2], + 0.0, + }; + self.updated = true; + } + + pub fn setMovementSpeed(self: *@This(), speed: f32) void { + self.movement_speed = speed; + } + + pub fn setPerspective(self: *@This(), fov: f32, aspect: f32, znear: f32, zfar: f32) void { + self.fov = fov; + self.znear = znear; + self.zfar = zfar; + const perspective = zm.perspectiveFovRhGl(toRadians(fov), aspect, znear, zfar); + self.matrices.perspective[0] = perspective[0]; + self.matrices.perspective[1] = perspective[1]; + self.matrices.perspective[2] = perspective[2]; + self.matrices.perspective[3] = perspective[3]; + } + + pub fn setRotationSpeed(self: *@This(), speed: f32) void { + self.rotation_speed = speed; + } + + pub fn setRotation(self: *@This(), rotation: Vec3) void { + self.rotation = rotation; + self.updateViewMatrix(); + } + + pub fn rotate(self: *@This(), delta: Vec2) void { + self.rotation[0] -= delta[1]; + self.rotation[1] -= delta[0]; + self.updateViewMatrix(); + } + + pub fn setPosition(self: *@This(), position: Vec3) void { + self.position = .{ + position[0], + -position[1], + position[2], + }; + self.updateViewMatrix(); + } +}; + +const UniformBuffers = struct { + const Params = struct { + buffer: *gpu.Buffer, + buffer_size: u64, + model_size: u64, + }; + const Buffer = struct { + buffer: *gpu.Buffer, + size: u32, + }; + ubo_matrices: Buffer, + ubo_params: Buffer, + material_params: Params, + object_params: Params, +}; + +const UboParams = struct { + lights: [4]Vec4, +}; + +const UboMatrices = extern struct { + projection: Mat4, + model: Mat4, + view: Mat4, + camera_position: Vec3, +}; + +const grid_element_count = grid_dimensions * grid_dimensions; + +const MaterialParamsDynamic = extern struct { + roughness: f32 = 0, + metallic: f32 = 0, + color: Vec3 = .{ 0, 0, 0 }, + padding: [236]u8 = [1]u8{0} ** 236, +}; +const MaterialParamsDynamicGrid = [grid_element_count]MaterialParamsDynamic; + +const ObjectParamsDynamic = extern struct { + position: Vec3 = .{ 0, 0, 0 }, + padding: [244]u8 = [1]u8{0} ** 244, +}; +const ObjectParamsDynamicGrid = [grid_element_count]ObjectParamsDynamic; + +// +// Globals +// + +const material_names = [11][:0]const u8{ + "Gold", "Copper", "Chromium", "Nickel", "Titanium", "Cobalt", "Platinum", + // Testing materials + "White", "Red", "Blue", "Black", +}; + +const object_names = [5][:0]const u8{ "Sphere", "Teapot", "Torusknot", "Venus", "Stanford Dragon" }; + +const materials = [_]Material{ + .{ .name = "Gold", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 1.0, 0.765557, 0.336057 } } }, + .{ .name = "Copper", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.955008, 0.637427, 0.538163 } } }, + .{ .name = "Chromium", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.549585, 0.556114, 0.554256 } } }, + .{ .name = "Nickel", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 1.0, 0.608679, 0.525649 } } }, + .{ .name = "Titanium", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.541931, 0.496791, 0.449419 } } }, + .{ .name = "Cobalt", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.662124, 0.654864, 0.633732 } } }, + .{ .name = "Platinum", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.672411, 0.637331, 0.585456 } } }, + // Testing colors + .{ .name = "White", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 1.0, 1.0, 1.0 } } }, + .{ .name = "Red", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 1.0, 0.0, 0.0 } } }, + .{ .name = "Blue", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.0, 0.0, 1.0 } } }, + .{ .name = "Black", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.0, 0.0, 0.0 } } }, +}; + +const grid_dimensions = 7; +const model_embeds = [_][:0]const u8{ + assets.sphere_m3d, + assets.teapot_m3d, + assets.torusknot_m3d, + assets.venus_m3d, + assets.stanford_dragon_m3d, +}; + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +// +// Member variables +// + +title_timer: core.Timer, +timer: core.Timer, +camera: Camera, +render_pipeline: *gpu.RenderPipeline, +render_pass_descriptor: gpu.RenderPassDescriptor, +bind_group: *gpu.BindGroup, +color_attachment: gpu.RenderPassColorAttachment, +depth_stencil_attachment_description: gpu.RenderPassDepthStencilAttachment, +depth_texture: *gpu.Texture, +depth_texture_view: *gpu.TextureView, +pressed_keys: PressedKeys, +models: [5]Model, +ubo_params: UboParams, +ubo_matrices: UboMatrices, +uniform_buffers: UniformBuffers, +material_params_dynamic: MaterialParamsDynamicGrid = [1]MaterialParamsDynamic{.{}} ** grid_element_count, +object_params_dynamic: ObjectParamsDynamicGrid = [1]ObjectParamsDynamic{.{}} ** grid_element_count, +uniform_buffers_dirty: bool, +buffers_bound: bool, +is_paused: bool, +current_material_index: usize, +current_object_index: usize, +mouse_position: core.Position, +is_rotating: bool, + +// +// Functions +// + +pub fn init(app: *App) !void { + try core.init(.{}); + app.timer = try core.Timer.start(); + app.title_timer = try core.Timer.start(); + + app.pressed_keys = .{}; + app.buffers_bound = false; + app.is_paused = false; + app.uniform_buffers_dirty = false; + app.current_material_index = 0; + app.current_object_index = 0; + app.mouse_position = .{ .x = 0, .y = 0 }; + app.is_rotating = false; + + setupCamera(app); + try loadModels(std.heap.c_allocator, app); + prepareUniformBuffers(app); + setupPipeline(app); + setupRenderPass(app); + app.printControls(); +} + +pub fn deinit(app: *App) void { + defer _ = gpa.deinit(); + defer core.deinit(); + + app.bind_group.release(); + app.render_pipeline.release(); + app.depth_texture_view.release(); + app.depth_texture.release(); + for (app.models) |model| { + model.index_buffer.release(); + model.vertex_buffer.release(); + } + app.uniform_buffers.ubo_matrices.buffer.release(); + app.uniform_buffers.ubo_params.buffer.release(); + app.uniform_buffers.material_params.buffer.release(); + app.uniform_buffers.object_params.buffer.release(); +} + +pub fn update(app: *App) !bool { + var iter = core.pollEvents(); + while (iter.next()) |event| { + app.updateUI(event); + switch (event) { + .mouse_motion => |ev| { + if (app.is_rotating) { + const delta = Vec2{ + @as(f32, @floatCast((app.mouse_position.x - ev.pos.x) * app.camera.rotation_speed)), + @as(f32, @floatCast((app.mouse_position.y - ev.pos.y) * app.camera.rotation_speed)), + }; + app.mouse_position = ev.pos; + app.camera.rotate(delta); + app.uniform_buffers_dirty = true; + } + }, + .mouse_press => |ev| { + if (ev.button == .left) { + app.is_rotating = true; + app.mouse_position = ev.pos; + } + }, + .mouse_release => |ev| { + if (ev.button == .left) { + app.is_rotating = false; + } + }, + .key_press, .key_repeat => |ev| { + const key = ev.key; + if (key == .up or key == .w) app.pressed_keys.up = true; + if (key == .down or key == .s) app.pressed_keys.down = true; + if (key == .left or key == .a) app.pressed_keys.left = true; + if (key == .right or key == .d) app.pressed_keys.right = true; + }, + .framebuffer_resize => |ev| { + app.depth_texture_view.release(); + app.depth_texture.release(); + app.depth_texture = core.device.createTexture(&gpu.Texture.Descriptor{ + .usage = .{ .render_attachment = true }, + .format = .depth24_plus_stencil8, + .sample_count = 1, + .size = .{ + .width = ev.width, + .height = ev.height, + .depth_or_array_layers = 1, + }, + }); + app.depth_texture_view = app.depth_texture.createView(&gpu.TextureView.Descriptor{ + .format = .depth24_plus_stencil8, + .dimension = .dimension_2d, + .array_layer_count = 1, + .aspect = .all, + }); + app.depth_stencil_attachment_description = gpu.RenderPassDepthStencilAttachment{ + .view = app.depth_texture_view, + .depth_load_op = .clear, + .depth_store_op = .store, + .depth_clear_value = 1.0, + .stencil_clear_value = 0, + .stencil_load_op = .clear, + .stencil_store_op = .store, + }; + + const aspect_ratio = @as(f32, @floatFromInt(ev.width)) / @as(f32, @floatFromInt(ev.height)); + app.camera.setPerspective(60.0, aspect_ratio, 0.1, 256.0); + app.uniform_buffers_dirty = true; + }, + .close => return true, + else => {}, + } + } + if (app.pressed_keys.areKeysPressed()) { + app.camera.calculateMovement(app.pressed_keys); + app.pressed_keys.clear(); + app.uniform_buffers_dirty = true; + } + + if (app.uniform_buffers_dirty) { + updateUniformBuffers(app); + app.uniform_buffers_dirty = false; + } + + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + app.color_attachment.view = back_buffer_view; + app.render_pass_descriptor = gpu.RenderPassDescriptor{ + .color_attachment_count = 1, + .color_attachments = &[_]gpu.RenderPassColorAttachment{app.color_attachment}, + .depth_stencil_attachment = &app.depth_stencil_attachment_description, + }; + const encoder = core.device.createCommandEncoder(null); + const current_model = app.models[app.current_object_index]; + + const pass = encoder.beginRenderPass(&app.render_pass_descriptor); + + const dimensions = Dimensions2D(f32){ + .width = @as(f32, @floatFromInt(core.descriptor.width)), + .height = @as(f32, @floatFromInt(core.descriptor.height)), + }; + pass.setViewport( + 0, + 0, + dimensions.width, + dimensions.height, + 0.0, + 1.0, + ); + pass.setScissorRect(0, 0, core.descriptor.width, core.descriptor.height); + pass.setPipeline(app.render_pipeline); + + if (!app.is_paused) { + app.updateLights(); + } + + var i: usize = 0; + while (i < (grid_dimensions * grid_dimensions)) : (i += 1) { + const alignment = 256; + const dynamic_offset: u32 = @as(u32, @intCast(i)) * alignment; + const dynamic_offsets = [2]u32{ dynamic_offset, dynamic_offset }; + pass.setBindGroup(0, app.bind_group, &dynamic_offsets); + if (!app.buffers_bound) { + pass.setVertexBuffer(0, current_model.vertex_buffer, 0, @sizeOf(Vertex) * current_model.vertex_count); + pass.setIndexBuffer(current_model.index_buffer, .uint32, 0, gpu.whole_size); + app.buffers_bound = true; + } + pass.drawIndexed( + current_model.index_count, // index_count + 1, // instance_count + 0, // first_index + 0, // base_vertex + 0, // first_instance + ); + } + + pass.end(); + pass.release(); + + var command = encoder.finish(null); + encoder.release(); + + const queue = core.queue; + queue.submit(&[_]*gpu.CommandBuffer{command}); + + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); + app.buffers_bound = false; + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("PBR Basic [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} + +fn prepareUniformBuffers(app: *App) void { + comptime { + std.debug.assert(@sizeOf(ObjectParamsDynamic) == 256); + std.debug.assert(@sizeOf(MaterialParamsDynamic) == 256); + } + + app.uniform_buffers.ubo_matrices.size = roundToMultipleOf4(u32, @as(u32, @intCast(@sizeOf(UboMatrices)))) + 4; + app.uniform_buffers.ubo_matrices.buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = app.uniform_buffers.ubo_matrices.size, + .mapped_at_creation = .false, + }); + + app.uniform_buffers.ubo_params.size = roundToMultipleOf4(u32, @as(u32, @intCast(@sizeOf(UboParams)))) + 4; + app.uniform_buffers.ubo_params.buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = app.uniform_buffers.ubo_params.size, + .mapped_at_creation = .false, + }); + + // + // Material parameter uniform buffer + // + app.uniform_buffers.material_params.model_size = @sizeOf(Vec2) + @sizeOf(Vec3); + app.uniform_buffers.material_params.buffer_size = calculateConstantBufferByteSize(@sizeOf(MaterialParamsDynamicGrid)); + std.debug.assert(app.uniform_buffers.material_params.buffer_size >= app.uniform_buffers.material_params.model_size); + app.uniform_buffers.material_params.buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = app.uniform_buffers.material_params.buffer_size, + .mapped_at_creation = .false, + }); + + // + // Object parameter uniform buffer + // + app.uniform_buffers.object_params.model_size = @sizeOf(Vec3) + 4; + app.uniform_buffers.object_params.buffer_size = calculateConstantBufferByteSize(@sizeOf(MaterialParamsDynamicGrid)) + 4; + std.debug.assert(app.uniform_buffers.object_params.buffer_size >= app.uniform_buffers.object_params.model_size); + app.uniform_buffers.object_params.buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = app.uniform_buffers.object_params.buffer_size, + .mapped_at_creation = .false, + }); + + app.updateUniformBuffers(); + app.updateDynamicUniformBuffer(); + app.updateLights(); +} + +fn updateDynamicUniformBuffer(app: *App) void { + var index: u32 = 0; + var y: usize = 0; + while (y < grid_dimensions) : (y += 1) { + var x: usize = 0; + while (x < grid_dimensions) : (x += 1) { + const grid_dimensions_float = @as(f32, @floatFromInt(grid_dimensions)); + app.object_params_dynamic[index].position[0] = (@as(f32, @floatFromInt(x)) - (grid_dimensions_float / 2) * 2.5); + app.object_params_dynamic[index].position[1] = 0; + app.object_params_dynamic[index].position[2] = (@as(f32, @floatFromInt(y)) - (grid_dimensions_float / 2) * 2.5); + app.material_params_dynamic[index].metallic = zm.clamp(@as(f32, @floatFromInt(x)) / (grid_dimensions_float - 1), 0.1, 1.0); + app.material_params_dynamic[index].roughness = zm.clamp(@as(f32, @floatFromInt(y)) / (grid_dimensions_float - 1), 0.05, 1.0); + app.material_params_dynamic[index].color = materials[app.current_material_index].params.color; + index += 1; + } + } + const queue = core.queue; + queue.writeBuffer( + app.uniform_buffers.object_params.buffer, + 0, + &app.object_params_dynamic, + ); + queue.writeBuffer( + app.uniform_buffers.material_params.buffer, + 0, + &app.material_params_dynamic, + ); +} + +fn updateUniformBuffers(app: *App) void { + app.ubo_matrices.projection = app.camera.matrices.perspective; + app.ubo_matrices.view = app.camera.matrices.view; + const rotation_degrees = if (app.current_object_index == 1) @as(f32, -45.0) else @as(f32, -90.0); + const model = zm.rotationY(rotation_degrees); + app.ubo_matrices.model[0] = model[0]; + app.ubo_matrices.model[1] = model[1]; + app.ubo_matrices.model[2] = model[2]; + app.ubo_matrices.model[3] = model[3]; + app.ubo_matrices.camera_position = .{ + -app.camera.position[0], + -app.camera.position[1], + -app.camera.position[2], + }; + const queue = core.queue; + queue.writeBuffer(app.uniform_buffers.ubo_matrices.buffer, 0, &[_]UboMatrices{app.ubo_matrices}); +} + +fn updateLights(app: *App) void { + const p: f32 = 15.0; + app.ubo_params.lights[0] = Vec4{ -p, -p * 0.5, -p, 1.0 }; + app.ubo_params.lights[1] = Vec4{ -p, -p * 0.5, p, 1.0 }; + app.ubo_params.lights[2] = Vec4{ p, -p * 0.5, p, 1.0 }; + app.ubo_params.lights[3] = Vec4{ p, -p * 0.5, -p, 1.0 }; + const base_value = toRadians(@mod(app.timer.read() * 0.1, 1.0) * 360.0); + app.ubo_params.lights[0][0] = @sin(base_value) * 20.0; + app.ubo_params.lights[0][2] = @cos(base_value) * 20.0; + app.ubo_params.lights[1][0] = @cos(base_value) * 20.0; + app.ubo_params.lights[1][1] = @sin(base_value) * 20.0; + const queue = core.queue; + queue.writeBuffer( + app.uniform_buffers.ubo_params.buffer, + 0, + &[_]UboParams{app.ubo_params}, + ); +} + +fn setupPipeline(app: *App) void { + comptime { + std.debug.assert(@sizeOf(Vertex) == @sizeOf(f32) * 6); + } + + const bind_group_layout_entries = [_]gpu.BindGroupLayout.Entry{ + .{ + .binding = 0, + .visibility = .{ .vertex = true, .fragment = true }, + .buffer = .{ + .type = .uniform, + .has_dynamic_offset = .false, + .min_binding_size = app.uniform_buffers.ubo_matrices.size, + }, + }, + .{ + .binding = 1, + .visibility = .{ .fragment = true }, + .buffer = .{ + .type = .uniform, + .has_dynamic_offset = .false, + .min_binding_size = app.uniform_buffers.ubo_params.size, + }, + }, + .{ + .binding = 2, + .visibility = .{ .fragment = true }, + .buffer = .{ + .type = .uniform, + .has_dynamic_offset = .true, + .min_binding_size = app.uniform_buffers.material_params.model_size, + }, + }, + .{ + .binding = 3, + .visibility = .{ .vertex = true }, + .buffer = .{ + .type = .uniform, + .has_dynamic_offset = .true, + .min_binding_size = app.uniform_buffers.object_params.model_size, + }, + }, + }; + + const bind_group_layout = core.device.createBindGroupLayout( + &gpu.BindGroupLayout.Descriptor.init(.{ + .entries = bind_group_layout_entries[0..], + }), + ); + defer bind_group_layout.release(); + + const bind_group_layouts = [_]*gpu.BindGroupLayout{bind_group_layout}; + const pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &bind_group_layouts, + })); + defer pipeline_layout.release(); + + const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{ + .array_stride = @sizeOf(Vertex), + .step_mode = .vertex, + .attributes = &.{ + .{ .format = .float32x3, .offset = @offsetOf(Vertex, "position"), .shader_location = 0 }, + .{ .format = .float32x3, .offset = @offsetOf(Vertex, "normal"), .shader_location = 1 }, + }, + }); + + const blend_component_descriptor = gpu.BlendComponent{ + .operation = .add, + .src_factor = .one, + .dst_factor = .zero, + }; + + const color_target_state = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &.{ + .color = blend_component_descriptor, + .alpha = blend_component_descriptor, + }, + }; + + const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .layout = pipeline_layout, + .primitive = .{ + .cull_mode = .back, + }, + .depth_stencil = &.{ + .format = .depth24_plus_stencil8, + .depth_write_enabled = .true, + .depth_compare = .less, + }, + .fragment = &gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "frag_main", + .targets = &.{color_target_state}, + }), + .vertex = gpu.VertexState.init(.{ + .module = shader_module, + .entry_point = "vertex_main", + .buffers = &.{vertex_buffer_layout}, + }), + }; + app.render_pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + shader_module.release(); + + { + const bind_group_entries = [_]gpu.BindGroup.Entry{ + .{ + .binding = 0, + .buffer = app.uniform_buffers.ubo_matrices.buffer, + .size = app.uniform_buffers.ubo_matrices.size, + }, + .{ + .binding = 1, + .buffer = app.uniform_buffers.ubo_params.buffer, + .size = app.uniform_buffers.ubo_params.size, + }, + .{ + .binding = 2, + .buffer = app.uniform_buffers.material_params.buffer, + .size = app.uniform_buffers.material_params.model_size, + }, + .{ + .binding = 3, + .buffer = app.uniform_buffers.object_params.buffer, + .size = app.uniform_buffers.object_params.model_size, + }, + }; + app.bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bind_group_layout, + .entries = &bind_group_entries, + }), + ); + } +} + +fn setupRenderPass(app: *App) void { + app.color_attachment = gpu.RenderPassColorAttachment{ + .clear_value = .{ + .r = 0.0, + .g = 0.0, + .b = 0.0, + .a = 0.0, + }, + .load_op = .clear, + .store_op = .store, + }; + + app.depth_texture = core.device.createTexture(&.{ + .usage = .{ .render_attachment = true, .copy_src = true }, + .format = .depth24_plus_stencil8, + .sample_count = 1, + .size = .{ + .width = core.descriptor.width, + .height = core.descriptor.height, + .depth_or_array_layers = 1, + }, + }); + + app.depth_texture_view = app.depth_texture.createView(&.{ + .format = .depth24_plus_stencil8, + .dimension = .dimension_2d, + .array_layer_count = 1, + .aspect = .all, + }); + + app.depth_stencil_attachment_description = gpu.RenderPassDepthStencilAttachment{ + .view = app.depth_texture_view, + .depth_load_op = .clear, + .depth_store_op = .store, + .depth_clear_value = 1.0, + .stencil_clear_value = 0, + .stencil_load_op = .clear, + .stencil_store_op = .store, + }; +} + +fn loadModels(allocator: std.mem.Allocator, app: *App) !void { + for (model_embeds, 0..) |model_data, model_data_i| { + const m3d_model = m3d.load(model_data, null, null, null) orelse return error.LoadModelFailed; + + const vertex_count = m3d_model.handle.numvertex; + const face_count = m3d_model.handle.numface; + + var model: *Model = &app.models[model_data_i]; + + model.index_count = face_count * 3; + + var vertex_writer = try VertexWriter(Vertex, u32).init(allocator, face_count * 3, vertex_count, face_count * 3); + defer vertex_writer.deinit(allocator); + + const scale: f32 = 0.45; + const vertices = m3d_model.handle.vertex[0..vertex_count]; + var i: usize = 0; + while (i < face_count) : (i += 1) { + const face = m3d_model.handle.face[i]; + var x: usize = 0; + while (x < 3) : (x += 1) { + const vertex_index = face.vertex[x]; + const normal_index = face.normal[x]; + const vertex = Vertex{ + .position = .{ + vertices[vertex_index].x * scale, + vertices[vertex_index].y * scale, + vertices[vertex_index].z * scale, + }, + .normal = .{ + vertices[normal_index].x, + vertices[normal_index].y, + vertices[normal_index].z, + }, + }; + vertex_writer.put(vertex, vertex_index); + } + } + + const vertex_buffer = vertex_writer.vertexBuffer(); + const index_buffer = vertex_writer.indexBuffer(); + + model.vertex_count = @as(u32, @intCast(vertex_buffer.len)); + + model.vertex_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .vertex = true }, + .size = @sizeOf(Vertex) * model.vertex_count, + .mapped_at_creation = .false, + }); + const queue = core.queue; + queue.writeBuffer(model.vertex_buffer, 0, vertex_buffer); + + model.index_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .index = true }, + .size = @sizeOf(u32) * model.index_count, + .mapped_at_creation = .false, + }); + queue.writeBuffer(model.index_buffer, 0, index_buffer); + } +} + +fn printControls(app: *App) void { + std.debug.print("[controls]\n", .{}); + std.debug.print("[p] paused: {}\n", .{app.is_paused}); + std.debug.print("[m] material: {s}\n", .{material_names[app.current_material_index]}); + std.debug.print("[o] object: {s}\n", .{object_names[app.current_object_index]}); +} + +fn updateUI(app: *App, event: core.Event) void { + switch (event) { + .key_press => |ev| { + var update_uniform_buffers: bool = false; + switch (ev.key) { + .p => app.is_paused = !app.is_paused, + .m => { + app.current_material_index = (app.current_material_index + 1) % material_names.len; + update_uniform_buffers = true; + }, + .o => { + app.current_object_index = (app.current_object_index + 1) % object_names.len; + update_uniform_buffers = true; + }, + else => return, + } + app.printControls(); + if (update_uniform_buffers) { + updateDynamicUniformBuffer(app); + } + }, + else => {}, + } +} + +fn setupCamera(app: *App) void { + app.camera = Camera{ + .rotation_speed = 1.0, + .movement_speed = 1.0, + }; + const aspect_ratio: f32 = @as(f32, @floatFromInt(core.descriptor.width)) / @as(f32, @floatFromInt(core.descriptor.height)); + app.camera.setPosition(.{ 10.0, 6.0, 6.0 }); + app.camera.setRotation(.{ 62.5, 90.0, 0.0 }); + app.camera.setMovementSpeed(0.5); + app.camera.setPerspective(60.0, aspect_ratio, 0.1, 256.0); + app.camera.setRotationSpeed(0.25); +} + +inline fn roundToMultipleOf4(comptime T: type, value: T) T { + return (value + 3) & ~@as(T, 3); +} + +inline fn calculateConstantBufferByteSize(byte_size: usize) usize { + return (byte_size + 255) & ~@as(usize, 255); +} + +inline fn toRadians(degrees: f32) f32 { + return degrees * (std.math.pi / 180.0); +} diff --git a/src/core/examples/sysgpu/pbr-basic/shader.wgsl b/src/core/examples/sysgpu/pbr-basic/shader.wgsl new file mode 100644 index 00000000..07cb4748 --- /dev/null +++ b/src/core/examples/sysgpu/pbr-basic/shader.wgsl @@ -0,0 +1,119 @@ +@group(0) @binding(0) var ubo : UBO; +@group(0) @binding(1) var uboParams : UBOShared; +@group(0) @binding(2) var material : MaterialParams; +@group(0) @binding(3) var object : ObjectParams; + +struct VertexOut { + @builtin(position) position_clip : vec4, + @location(0) fragPosition : vec3, + @location(1) fragNormal : vec3, +} + +struct MaterialParams { + roughness : f32, + metallic : f32, + r : f32, + g : f32, + b : f32 +} + +struct UBOShared { + lights : array, 4>, +} + +struct UBO { + projection : mat4x4, + model : mat4x4, + view : mat4x4, + camPos : vec3, +} + +struct ObjectParams { + position : vec3 +} + +@vertex fn vertex_main( + @location(0) position : vec3, + @location(1) normal : vec3 +) -> VertexOut { + var output : VertexOut; + var locPos = vec4(ubo.model * vec4(position, 1.0)); + output.fragPosition = locPos.xyz + object.position; + output.fragNormal = mat3x3(ubo.model[0].xyz, ubo.model[1].xyz, ubo.model[2].xyz) * normal; + output.position_clip = ubo.projection * ubo.view * vec4(output.fragPosition, 1.0); + return output; +} + +const PI : f32 = 3.14159265359; + +fn material_color() -> vec3 { + return vec3(material.r, material.g, material.b); +} + +// Normal Distribution function -------------------------------------- +fn D_GGX(dotNH : f32, roughness : f32) -> f32 { + var alpha : f32 = roughness * roughness; + var alpha2 : f32 = alpha * alpha; + var denom : f32 = dotNH * dotNH * (alpha2 - 1.0) + 1.0; + return alpha2 / (PI * denom * denom); +} + +// Geometric Shadowing function -------------------------------------- +fn G_SchlicksmithGGX(dotNL : f32, dotNV : f32, roughness : f32) -> f32 { + var r : f32 = roughness + 1.0; + var k : f32 = (r * r) / 8.0; + var GL : f32 = dotNL / (dotNL * (1.0 - k) + k); + var GV : f32 = dotNV / (dotNV * (1.0 - k) + k); + return GL * GV; +} + +// Fresnel function ---------------------------------------------------- +fn F_Schlick(cosTheta : f32, metallic : f32) -> vec3 { + var F0 : vec3 = mix(vec3(0.04), material_color(), metallic); + var F : vec3 = F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); + return F; +} + +// Specular BRDF composition -------------------------------------------- +fn BRDF(L : vec3, V : vec3, N : vec3, metallic : f32, roughness : f32) -> vec3 { + var H : vec3 = normalize(V + L); + var dotNV : f32 = clamp(dot(N, V), 0.0, 1.0); + var dotNL : f32 = clamp(dot(N, L), 0.0, 1.0); + var dotLH : f32 = clamp(dot(L, H), 0.0, 1.0); + var dotNH : f32 = clamp(dot(N, H), 0.0, 1.0); + var lightColor = vec3(1.0); + var color = vec3(0.0); + if(dotNL > 0.0) { + var rroughness : f32 = max(0.05, roughness); + // D = Normal distribution (Distribution of the microfacets) + var D : f32 = D_GGX(dotNH, roughness); + // G = Geometric shadowing term (Microfacets shadowing) + var G : f32 = G_SchlicksmithGGX(dotNL, dotNV, roughness); + // F = Fresnel factor (Reflectance depending on angle of incidence) + var F : vec3 = F_Schlick(dotNV, metallic); + var spec : vec3 = (D * F * G) / (4.0 * dotNL * dotNV); + color += spec * dotNL * lightColor; + } + return color; +} + +// TODO - global variable declaration order +@fragment fn frag_main( + @location(0) position : vec3, + @location(1) normal: vec3 +) -> @location(0) vec4 { + var N : vec3 = normalize(normal); + var V : vec3 = normalize(ubo.camPos - position); + var Lo = vec3(0.0); + // Specular contribution + for(var i: i32 = 0; i < 4; i++) { + var L : vec3 = normalize(uboParams.lights[i].xyz - position); + Lo += BRDF(L, V, N, material.metallic, material.roughness); + } + // Combine with ambient + var color : vec3 = material_color() * 0.02; + color += Lo; + // Gamma correct + color = pow(color, vec3(0.4545)); + return vec4(color, 1.0); +} diff --git a/src/core/examples/sysgpu/pbr-basic/vertex_writer.zig b/src/core/examples/sysgpu/pbr-basic/vertex_writer.zig new file mode 100644 index 00000000..1610981e --- /dev/null +++ b/src/core/examples/sysgpu/pbr-basic/vertex_writer.zig @@ -0,0 +1,188 @@ +const std = @import("std"); + +/// Vertex writer manages the placement of vertices by tracking which are unique. If a duplicate vertex is added +/// with `put`, only it's index will be written to the index buffer. +/// `IndexType` should match the integer type used for the index buffer +pub fn VertexWriter(comptime VertexType: type, comptime IndexType: type) type { + return struct { + const MapEntry = struct { + packed_index: IndexType = null_index, + next_sparse: IndexType = null_index, + }; + + const null_index: IndexType = std.math.maxInt(IndexType); + + vertices: []VertexType, + indices: []IndexType, + sparse_to_packed_map: []MapEntry, + + /// Next index outside of the 1:1 mapping range for storing + /// position -> normal collisions + next_collision_index: IndexType, + + /// Next packed index + next_packed_index: IndexType, + written_indices_count: IndexType, + + /// Allocate storage and set default values + /// `sparse_vertices_count` is the number of vertices in the source before de-duplication / remapping + /// Put more succinctly, the largest index value in source index buffer + /// `max_vertex_count` is largest permutation of vertices assuming that {vertex, uv, normal} never map 1:1 and always + /// create a new mapping + pub fn init( + allocator: std.mem.Allocator, + indices_count: IndexType, + sparse_vertices_count: IndexType, + max_vertex_count: IndexType, + ) !@This() { + var result: @This() = undefined; + result.vertices = try allocator.alloc(VertexType, max_vertex_count); + result.indices = try allocator.alloc(IndexType, indices_count); + result.sparse_to_packed_map = try allocator.alloc(MapEntry, max_vertex_count); + result.next_collision_index = sparse_vertices_count; + result.next_packed_index = 0; + result.written_indices_count = 0; + @memset(result.sparse_to_packed_map, .{}); + return result; + } + + pub fn put(self: *@This(), vertex: VertexType, sparse_index: IndexType) void { + if (self.sparse_to_packed_map[sparse_index].packed_index == null_index) { + // New start of chain, reserve a new packed index and add entry to `index_map` + const packed_index = self.next_packed_index; + self.sparse_to_packed_map[sparse_index].packed_index = packed_index; + self.vertices[packed_index] = vertex; + self.indices[self.written_indices_count] = packed_index; + self.written_indices_count += 1; + self.next_packed_index += 1; + return; + } + var previous_sparse_index: IndexType = undefined; + var current_sparse_index = sparse_index; + while (current_sparse_index != null_index) { + const packed_index = self.sparse_to_packed_map[current_sparse_index].packed_index; + if (std.mem.eql(u8, &std.mem.toBytes(self.vertices[packed_index]), &std.mem.toBytes(vertex))) { + // We already have a record for this vertex in our chain + self.indices[self.written_indices_count] = packed_index; + self.written_indices_count += 1; + return; + } + previous_sparse_index = current_sparse_index; + current_sparse_index = self.sparse_to_packed_map[current_sparse_index].next_sparse; + } + // This is a new mapping for the given sparse index + const packed_index = self.next_packed_index; + const remapped_sparse_index = self.next_collision_index; + self.indices[self.written_indices_count] = packed_index; + self.vertices[packed_index] = vertex; + self.sparse_to_packed_map[previous_sparse_index].next_sparse = remapped_sparse_index; + self.sparse_to_packed_map[remapped_sparse_index].packed_index = packed_index; + self.next_packed_index += 1; + self.next_collision_index += 1; + self.written_indices_count += 1; + } + + pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void { + allocator.free(self.vertices); + allocator.free(self.indices); + allocator.free(self.sparse_to_packed_map); + } + + pub fn indexBuffer(self: @This()) []IndexType { + return self.indices; + } + + pub fn vertexBuffer(self: @This()) []VertexType { + return self.vertices[0..self.next_packed_index]; + } + }; +} + +test "VertexWriter" { + const Vec3 = [3]f32; + const Vertex = extern struct { + position: Vec3, + normal: Vec3, + }; + + const expect = std.testing.expect; + const allocator = std.testing.allocator; + + const Face = struct { + position: [3]u16, + normal: [3]u16, + }; + + const vertices = [_]Vec3{ + Vec3{ 1.0, 0.0, 0.0 }, // 0: Position + Vec3{ 2.0, 0.0, 0.0 }, // 1: Position + Vec3{ 3.0, 0.0, 0.0 }, // 2: Position + Vec3{ 1.0, 0.0, 0.0 }, // 3: Normal + Vec3{ 4.0, 0.0, 0.0 }, // 4: Position + Vec3{ 0.0, 1.0, 0.0 }, // 5: Normal + Vec3{ 5.0, 0.0, 0.0 }, // 6: Position + Vec3{ 0.0, 0.0, 1.0 }, // 7: Normal + Vec3{ 1.0, 0.0, 1.0 }, // 8: Normal + Vec3{ 6.0, 0.0, 0.0 }, // 9: Position + }; + + const faces = [_]Face{ + .{ .position = .{ 0, 4, 2 }, .normal = .{ 7, 5, 3 } }, + .{ .position = .{ 2, 3, 9 }, .normal = .{ 3, 7, 8 } }, + .{ .position = .{ 9, 2, 4 }, .normal = .{ 8, 7, 5 } }, + .{ .position = .{ 2, 6, 1 }, .normal = .{ 3, 5, 7 } }, + .{ .position = .{ 9, 6, 0 }, .normal = .{ 5, 7, 8 } }, + }; + + var writer = try VertexWriter(Vertex, u32).init( + allocator, + faces.len * 3, // indices count + vertices.len, // original vertices count + faces.len * 3, // maximum vertices count + ); + defer writer.deinit(allocator); + + for (faces) |face| { + var x: usize = 0; + while (x < 3) : (x += 1) { + const position_index = face.position[x]; + const position = vertices[position_index]; + const normal = vertices[face.normal[x]]; + const vertex = Vertex{ + .position = position, + .normal = normal, + }; + writer.put(vertex, position_index); + } + } + + const indices = writer.indexBuffer(); + try expect(indices.len == faces.len * 3); + + // Face 0 + try expect(indices[0] == 0); // (0, 7) New + try expect(indices[1] == 1); // (4, 5) New + try expect(indices[2] == 2); // (2, 3) New + + // Face 1 + try expect(indices[3 + 0] == 2); // (2, 3) Duplicate - Reuse index + try expect(indices[3 + 1] == 3); // (3, 7) New + try expect(indices[3 + 2] == 4); // (9, 8) New + + // Face 2 + try expect(indices[6 + 0] == 4); // (9, 8) Duplicate - Reuse index + try expect(indices[6 + 1] == 5); // (2, 7) New normal mapping (Don't clobber) + try expect(indices[6 + 2] == 1); // (4, 5) Duplicate - Reuse Index + + // Face 3 + try expect(indices[9 + 0] == 2); // (2, 3) Duplicate - Reuse index + try expect(indices[9 + 1] == 6); // (6, 5) New + try expect(indices[9 + 2] == 7); // (1, 7) New + + // Face 4 + try expect(indices[12 + 0] == 8); // (9, 5) New normal mapping (Don't clobber) + try expect(indices[12 + 1] == 9); // (6, 7) New normal mapping (Don't clobber) + try expect(indices[12 + 2] == 10); // (0, 8) New normal mapping (Don't clobber) + + try expect(writer.vertexBuffer().len == 11); +} diff --git a/src/core/examples/sysgpu/pixel-post-process/cube_mesh.zig b/src/core/examples/sysgpu/pixel-post-process/cube_mesh.zig new file mode 100644 index 00000000..edf25840 --- /dev/null +++ b/src/core/examples/sysgpu/pixel-post-process/cube_mesh.zig @@ -0,0 +1,49 @@ +pub const Vertex = extern struct { + pos: @Vector(3, f32), + normal: @Vector(3, f32), + uv: @Vector(2, f32), +}; + +pub const vertices = [_]Vertex{ + .{ .pos = .{ 1, -1, 1 }, .normal = .{ 0, -1, 0 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, 1 }, .normal = .{ 0, -1, 0 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, -1, -1 }, .normal = .{ 0, -1, 0 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, -1, -1 }, .normal = .{ 0, -1, 0 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, -1, 1 }, .normal = .{ 0, -1, 0 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, -1 }, .normal = .{ 0, -1, 0 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ 1, 1, 1 }, .normal = .{ 1, 0, 0 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, -1, 1 }, .normal = .{ 1, 0, 0 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, -1, -1 }, .normal = .{ 1, 0, 0 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, 1, -1 }, .normal = .{ 1, 0, 0 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, 1, 1 }, .normal = .{ 1, 0, 0 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, -1, -1 }, .normal = .{ 1, 0, 0 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ -1, 1, 1 }, .normal = .{ 0, 1, 0 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, 1, 1 }, .normal = .{ 0, 1, 0 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, 1, -1 }, .normal = .{ 0, 1, 0 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, 1, -1 }, .normal = .{ 0, 1, 0 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, 1, 1 }, .normal = .{ 0, 1, 0 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, 1, -1 }, .normal = .{ 0, 1, 0 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ -1, -1, 1 }, .normal = .{ -1, 0, 0 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, 1 }, .normal = .{ -1, 0, 0 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, -1 }, .normal = .{ -1, 0, 0 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, -1, -1 }, .normal = .{ -1, 0, 0 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, -1, 1 }, .normal = .{ -1, 0, 0 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, -1 }, .normal = .{ -1, 0, 0 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ 1, 1, 1 }, .normal = .{ 0, 0, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, 1 }, .normal = .{ 0, 0, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, -1, 1 }, .normal = .{ 0, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, -1, 1 }, .normal = .{ 0, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, -1, 1 }, .normal = .{ 0, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, 1, 1 }, .normal = .{ 0, 0, 1 }, .uv = .{ 1, 1 } }, + + .{ .pos = .{ 1, -1, -1 }, .normal = .{ 0, 0, -1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, -1 }, .normal = .{ 0, 0, -1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, -1 }, .normal = .{ 0, 0, -1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, 1, -1 }, .normal = .{ 0, 0, -1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, -1, -1 }, .normal = .{ 0, 0, -1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, -1 }, .normal = .{ 0, 0, -1 }, .uv = .{ 0, 0 } }, +}; diff --git a/src/core/examples/sysgpu/pixel-post-process/main.zig b/src/core/examples/sysgpu/pixel-post-process/main.zig new file mode 100644 index 00000000..b3796fb2 --- /dev/null +++ b/src/core/examples/sysgpu/pixel-post-process/main.zig @@ -0,0 +1,466 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const zm = @import("zmath"); + +const Vertex = @import("cube_mesh.zig").Vertex; +const vertices = @import("cube_mesh.zig").vertices; +const Quad = @import("quad_mesh.zig").Quad; +const quad = @import("quad_mesh.zig").quad; + +pub const App = @This(); + +pub const mach_core_options = core.ComptimeOptions{ + .use_wgpu = false, + .use_sysgpu = true, +}; + +const pixel_size = 8; + +const UniformBufferObject = struct { + mat: zm.Mat, +}; +const PostUniformBufferObject = extern struct { + width: u32, + height: u32, + pixel_size: u32 = pixel_size, +}; +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +title_timer: core.Timer, +timer: core.Timer, + +pipeline: *gpu.RenderPipeline, +normal_pipeline: *gpu.RenderPipeline, +vertex_buffer: *gpu.Buffer, +uniform_buffer: *gpu.Buffer, +bind_group: *gpu.BindGroup, + +post_pipeline: *gpu.RenderPipeline, +post_vertex_buffer: *gpu.Buffer, +post_uniform_buffer: *gpu.Buffer, +post_bind_group: *gpu.BindGroup, + +draw_texture_view: *gpu.TextureView, +depth_texture_view: *gpu.TextureView, +normal_texture_view: *gpu.TextureView, + +pub fn init(app: *App) !void { + try core.init(.{}); + app.title_timer = try core.Timer.start(); + app.timer = try core.Timer.start(); + + try app.createRenderTextures(); + app.createDrawPipeline(); + app.createPostPipeline(); +} + +pub fn deinit(app: *App) void { + defer _ = gpa.deinit(); + defer core.deinit(); + + app.cleanup(); +} + +pub fn update(app: *App) !bool { + var iter = core.pollEvents(); + while (iter.next()) |event| { + switch (event) { + .key_press => |ev| { + if (ev.key == .space) return true; + }, + .close => return true, + .framebuffer_resize => { + app.cleanup(); + try app.createRenderTextures(); + app.createDrawPipeline(); + app.createPostPipeline(); + }, + else => {}, + } + } + + const size = core.size(); + const encoder = core.device.createCommandEncoder(null); + encoder.writeBuffer(app.post_uniform_buffer, 0, &[_]PostUniformBufferObject{ + PostUniformBufferObject{ + .width = size.width, + .height = size.height, + }, + }); + + { + const time = app.timer.read() * 0.5; + const model = zm.mul(zm.rotationX(time * (std.math.pi / 2.0)), zm.rotationZ(time * (std.math.pi / 2.0))); + const view = zm.lookAtRh( + zm.Vec{ 0, 5, 2, 1 }, + zm.Vec{ 0, 0, 0, 1 }, + zm.Vec{ 0, 0, 1, 0 }, + ); + const proj = zm.perspectiveFovRh( + (std.math.pi / 4.0), + @as(f32, @floatFromInt(core.descriptor.width)) / @as(f32, @floatFromInt(core.descriptor.height)), + 0.1, + 10, + ); + const mvp = zm.mul(zm.mul(model, view), proj); + const ubo = UniformBufferObject{ + .mat = zm.transpose(mvp), + }; + encoder.writeBuffer(app.uniform_buffer, 0, &[_]UniformBufferObject{ubo}); + } + + { + // render scene to downscaled texture + const color_attachment = gpu.RenderPassColorAttachment{ + .view = app.draw_texture_view, + .clear_value = std.mem.zeroes(gpu.Color), + .load_op = .clear, + .store_op = .store, + }; + const render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + .depth_stencil_attachment = &gpu.RenderPassDepthStencilAttachment{ + .view = app.depth_texture_view, + .depth_load_op = .clear, + .depth_store_op = .store, + .depth_clear_value = 1.0, + }, + }); + + const pass = encoder.beginRenderPass(&render_pass_info); + pass.setPipeline(app.pipeline); + pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len); + pass.setBindGroup(0, app.bind_group, &.{0}); + pass.draw(vertices.len, 1, 0, 0); + pass.end(); + pass.release(); + } + + { + // render scene normals to texture + const normal_color_attachment = gpu.RenderPassColorAttachment{ + .view = app.normal_texture_view, + .clear_value = .{ .r = 0.5, .b = 0.5, .g = 0.5, .a = 1.0 }, + .load_op = .clear, + .store_op = .store, + }; + const normal_render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{normal_color_attachment}, + }); + + const normal_pass = encoder.beginRenderPass(&normal_render_pass_info); + normal_pass.setPipeline(app.normal_pipeline); + normal_pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len); + normal_pass.setBindGroup(0, app.bind_group, &.{0}); + normal_pass.draw(vertices.len, 1, 0, 0); + normal_pass.end(); + normal_pass.release(); + } + + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + { + // render to swap chain using previous passes + const post_color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = std.mem.zeroes(gpu.Color), + .load_op = .clear, + .store_op = .store, + }; + const post_render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{post_color_attachment}, + }); + + const draw_pass = encoder.beginRenderPass(&post_render_pass_info); + draw_pass.setPipeline(app.post_pipeline); + draw_pass.setVertexBuffer(0, app.post_vertex_buffer, 0, @sizeOf(Quad) * quad.len); + draw_pass.setBindGroup(0, app.post_bind_group, &.{0}); + draw_pass.draw(quad.len, 1, 0, 0); + draw_pass.end(); + draw_pass.release(); + } + + var command = encoder.finish(null); + encoder.release(); + + const queue = core.queue; + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Pixel Post Process [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} + +fn cleanup(app: *App) void { + app.pipeline.release(); + app.normal_pipeline.release(); + app.vertex_buffer.release(); + app.uniform_buffer.release(); + app.bind_group.release(); + + app.post_pipeline.release(); + app.post_vertex_buffer.release(); + app.post_uniform_buffer.release(); + app.post_bind_group.release(); + + app.draw_texture_view.release(); + app.depth_texture_view.release(); + app.normal_texture_view.release(); +} + +fn createRenderTextures(app: *App) !void { + const size = core.size(); + + const draw_texture_desc = gpu.Texture.Descriptor.init(.{ + .size = .{ .width = size.width / pixel_size, .height = size.height / pixel_size }, + .format = .bgra8_unorm, + .usage = .{ .texture_binding = true, .copy_dst = true, .render_attachment = true }, + }); + const draw_texture = core.device.createTexture(&draw_texture_desc); + app.draw_texture_view = draw_texture.createView(null); + draw_texture.release(); + + const depth_texture_desc = gpu.Texture.Descriptor.init(.{ + .size = .{ .width = size.width / pixel_size, .height = size.height / pixel_size }, + .format = .depth32_float, + .usage = .{ .texture_binding = true, .copy_dst = true, .render_attachment = true }, + }); + const depth_texture = core.device.createTexture(&depth_texture_desc); + app.depth_texture_view = depth_texture.createView(null); + depth_texture.release(); + + const normal_texture_desc = gpu.Texture.Descriptor.init(.{ + .size = .{ .width = size.width / pixel_size, .height = size.height / pixel_size }, + .format = .bgra8_unorm, + .usage = .{ .texture_binding = true, .copy_dst = true, .render_attachment = true }, + }); + const normal_texture = core.device.createTexture(&normal_texture_desc); + app.normal_texture_view = normal_texture.createView(null); + normal_texture.release(); +} + +fn createDrawPipeline(app: *App) void { + const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); + const vertex_attributes = [_]gpu.VertexAttribute{ + .{ .format = .float32x3, .offset = @offsetOf(Vertex, "pos"), .shader_location = 0 }, + .{ .format = .float32x3, .offset = @offsetOf(Vertex, "normal"), .shader_location = 1 }, + .{ .format = .float32x2, .offset = @offsetOf(Vertex, "uv"), .shader_location = 2 }, + }; + const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{ + .array_stride = @sizeOf(Vertex), + .step_mode = .vertex, + .attributes = &vertex_attributes, + }); + const vertex = gpu.VertexState.init(.{ + .module = shader_module, + .entry_point = "vertex_main", + .buffers = &.{vertex_buffer_layout}, + }); + + const blend = gpu.BlendState{}; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + + const bgle = gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, true, 0); + const bgl = core.device.createBindGroupLayout( + &gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &.{bgle}, + }), + ); + + const bind_group_layouts = [_]*gpu.BindGroupLayout{bgl}; + const pipeline_layout = core.device.createPipelineLayout( + &gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &bind_group_layouts, + }), + ); + + const vertex_buffer = core.device.createBuffer(&.{ + .usage = .{ .vertex = true }, + .size = @sizeOf(Vertex) * vertices.len, + .mapped_at_creation = .true, + }); + const vertex_mapped = vertex_buffer.getMappedRange(Vertex, 0, vertices.len); + @memcpy(vertex_mapped.?, vertices[0..]); + vertex_buffer.unmap(); + + const uniform_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = @sizeOf(UniformBufferObject), + .mapped_at_creation = .false, + }); + const bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bgl, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, uniform_buffer, 0, @sizeOf(UniformBufferObject), @sizeOf(UniformBufferObject)), + }, + }), + ); + + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + .layout = pipeline_layout, + .vertex = vertex, + .primitive = .{ + .cull_mode = .back, + }, + .depth_stencil = &gpu.DepthStencilState{ + .format = .depth32_float, + .depth_write_enabled = .true, + .depth_compare = .less, + }, + }; + + { + // "same" pipeline, different fragment shader to create a texture with normal information + const normal_fs_module = core.device.createShaderModuleWGSL("normal_frag.wgsl", @embedFile("normal_frag.wgsl")); + const normal_fragment = gpu.FragmentState.init(.{ + .module = normal_fs_module, + .entry_point = "main", + .targets = &.{color_target}, + }); + const normal_pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &normal_fragment, + .layout = pipeline_layout, + .vertex = vertex, + .primitive = .{ + .cull_mode = .back, + }, + }; + app.normal_pipeline = core.device.createRenderPipeline(&normal_pipeline_descriptor); + + normal_fs_module.release(); + } + + app.pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + app.vertex_buffer = vertex_buffer; + app.uniform_buffer = uniform_buffer; + app.bind_group = bind_group; + + shader_module.release(); + pipeline_layout.release(); + bgl.release(); +} + +fn createPostPipeline(app: *App) void { + const vs_module = core.device.createShaderModuleWGSL("pixel_vert.wgsl", @embedFile("pixel_vert.wgsl")); + const vertex_attributes = [_]gpu.VertexAttribute{ + .{ .format = .float32x3, .offset = @offsetOf(Quad, "pos"), .shader_location = 0 }, + .{ .format = .float32x2, .offset = @offsetOf(Quad, "uv"), .shader_location = 1 }, + }; + const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{ + .array_stride = @sizeOf(Quad), + .step_mode = .vertex, + .attributes = &vertex_attributes, + }); + const vertex = gpu.VertexState.init(.{ + .module = vs_module, + .entry_point = "main", + .buffers = &.{vertex_buffer_layout}, + }); + + const fs_module = core.device.createShaderModuleWGSL("pixel_frag.wgsl", @embedFile("pixel_frag.wgsl")); + const blend = gpu.BlendState{}; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = gpu.FragmentState.init(.{ + .module = fs_module, + .entry_point = "main", + .targets = &.{color_target}, + }); + + const bgl = core.device.createBindGroupLayout( + &gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &.{ + gpu.BindGroupLayout.Entry.texture(0, .{ .fragment = true }, .float, .dimension_2d, false), + gpu.BindGroupLayout.Entry.sampler(1, .{ .fragment = true }, .filtering), + gpu.BindGroupLayout.Entry.texture(2, .{ .fragment = true }, .depth, .dimension_2d, false), + gpu.BindGroupLayout.Entry.sampler(3, .{ .fragment = true }, .filtering), + gpu.BindGroupLayout.Entry.texture(4, .{ .fragment = true }, .float, .dimension_2d, false), + gpu.BindGroupLayout.Entry.sampler(5, .{ .fragment = true }, .filtering), + gpu.BindGroupLayout.Entry.buffer(6, .{ .fragment = true }, .uniform, true, 0), + }, + }), + ); + + const bind_group_layouts = [_]*gpu.BindGroupLayout{bgl}; + const pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &bind_group_layouts, + })); + + const vertex_buffer = core.device.createBuffer(&.{ + .usage = .{ .vertex = true }, + .size = @sizeOf(Quad) * quad.len, + .mapped_at_creation = .true, + }); + const vertex_mapped = vertex_buffer.getMappedRange(Quad, 0, quad.len); + @memcpy(vertex_mapped.?, quad[0..]); + vertex_buffer.unmap(); + + const draw_sampler = core.device.createSampler(null); + const depth_sampler = core.device.createSampler(null); + const normal_sampler = core.device.createSampler(null); + const uniform_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = @sizeOf(PostUniformBufferObject), + .mapped_at_creation = .false, + }); + const bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bgl, + .entries = &[_]gpu.BindGroup.Entry{ + gpu.BindGroup.Entry.textureView(0, app.draw_texture_view), + gpu.BindGroup.Entry.sampler(1, draw_sampler), + gpu.BindGroup.Entry.textureView(2, app.depth_texture_view), + gpu.BindGroup.Entry.sampler(3, depth_sampler), + gpu.BindGroup.Entry.textureView(4, app.normal_texture_view), + gpu.BindGroup.Entry.sampler(5, normal_sampler), + gpu.BindGroup.Entry.buffer(6, uniform_buffer, 0, @sizeOf(PostUniformBufferObject), @sizeOf(PostUniformBufferObject)), + }, + }), + ); + draw_sampler.release(); + depth_sampler.release(); + normal_sampler.release(); + + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + .layout = pipeline_layout, + .vertex = vertex, + .primitive = .{ + .cull_mode = .back, + }, + }; + + app.post_pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + app.post_vertex_buffer = vertex_buffer; + app.post_uniform_buffer = uniform_buffer; + app.post_bind_group = bind_group; + + vs_module.release(); + fs_module.release(); + pipeline_layout.release(); + bgl.release(); +} diff --git a/src/core/examples/sysgpu/pixel-post-process/normal_frag.wgsl b/src/core/examples/sysgpu/pixel-post-process/normal_frag.wgsl new file mode 100644 index 00000000..28f7407c --- /dev/null +++ b/src/core/examples/sysgpu/pixel-post-process/normal_frag.wgsl @@ -0,0 +1,6 @@ +@fragment fn main( + @location(0) normal: vec3, + @location(1) uv: vec2, +) -> @location(0) vec4 { + return vec4(normal / 2 + 0.5, 1.0); +} diff --git a/src/core/examples/sysgpu/pixel-post-process/pixel_frag.wgsl b/src/core/examples/sysgpu/pixel-post-process/pixel_frag.wgsl new file mode 100644 index 00000000..9f125273 --- /dev/null +++ b/src/core/examples/sysgpu/pixel-post-process/pixel_frag.wgsl @@ -0,0 +1,77 @@ +@group(0) @binding(0) +var draw_texture: texture_2d; +@group(0) @binding(1) +var draw_texture_sampler: sampler; + +@group(0) @binding(2) +var depth_texture: texture_depth_2d; +@group(0) @binding(3) +var depth_texture_sampler: sampler; + +@group(0) @binding(4) +var normal_texture: texture_2d; +@group(0) @binding(5) +var normal_texture_sampler: sampler; + +struct View { + @location(0) width: u32, + @location(1) height: u32, + @location(2) pixel_size: u32, +} +@group(0) @binding(6) +var view: View; + +fn sample_depth(uv: vec2, x: f32, y: f32) -> f32 { + return textureSample( + depth_texture, + depth_texture_sampler, + uv + vec2(x * f32(view.pixel_size) / f32(view.width), y * f32(view.pixel_size) / f32(view.height)) + ); +} + +fn sample_normal(uv: vec2, x: f32, y: f32) -> vec3 { + return textureSample( + normal_texture, + normal_texture_sampler, + uv + vec2(x * f32(view.pixel_size) / f32(view.width), y * f32(view.pixel_size) / f32(view.height)) + ).xyz; +} + +fn normal_indicator(uv: vec2, x: f32, y: f32) -> f32 { + // TODO - integer promotion to float argument + var depth_diff = sample_depth(uv, 0.0, 0.0) - sample_depth(uv, x, y); + var dx = sample_normal(uv, 0.0, 0.0); + var dy = sample_normal(uv, x, y); + if (depth_diff > 0) { + // only sample normals from closest pixel + return 0; + } + return distance(dx, dy); +} + +@fragment fn main( + // TODO - vertex/fragment linkage + @location(0) uv: vec2, + @builtin(position) position: vec4 +) -> @location(0) vec4 { + // TODO - integer promotion to float argument + var depth = sample_depth(uv, 0.0, 0.0); + var depth_diff: f32 = 0; + depth_diff += abs(depth - sample_depth(uv, -1.0, 0.0)); + depth_diff += abs(depth - sample_depth(uv, 1.0, 0.0)); + depth_diff += abs(depth - sample_depth(uv, 0.0, -1.0)); + depth_diff += abs(depth - sample_depth(uv, 0.0, 1.0)); + + var normal_diff: f32 = 0; + normal_diff += normal_indicator(uv, -1.0, 0.0); + normal_diff += normal_indicator(uv, 1.0, 0.0); + normal_diff += normal_indicator(uv, 0.0, -1.0); + normal_diff += normal_indicator(uv, 0.0, 1.0); + + var color = textureSample(draw_texture, draw_texture_sampler, uv); + if (depth_diff > 0.007) { // magic number from testing + return color * 0.7; + } + // add instead of multiply so really dark pixels get brighter + return color + (vec4(1) * step(0.1, normal_diff) * 0.7); +} diff --git a/src/core/examples/sysgpu/pixel-post-process/pixel_vert.wgsl b/src/core/examples/sysgpu/pixel-post-process/pixel_vert.wgsl new file mode 100644 index 00000000..734f629e --- /dev/null +++ b/src/core/examples/sysgpu/pixel-post-process/pixel_vert.wgsl @@ -0,0 +1,14 @@ +struct VertexOut { + @builtin(position) position_clip: vec4, + @location(0) uv: vec2 +} + +@vertex fn main( + @location(0) position: vec3, + @location(1) uv: vec2 +) -> VertexOut { + var output : VertexOut; + output.position_clip = vec4(position.xy, 0.0, 1.0); + output.uv = uv; + return output; +} diff --git a/src/core/examples/sysgpu/pixel-post-process/quad_mesh.zig b/src/core/examples/sysgpu/pixel-post-process/quad_mesh.zig new file mode 100644 index 00000000..57db212f --- /dev/null +++ b/src/core/examples/sysgpu/pixel-post-process/quad_mesh.zig @@ -0,0 +1,13 @@ +pub const Quad = extern struct { + pos: @Vector(3, f32), + uv: @Vector(2, f32), +}; + +pub const quad = [_]Quad{ + .{ .pos = .{ -1.0, 1.0, 0.0 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1.0, -1.0, 0.0 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1.0, 1.0, 0.0 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1.0, 1.0, 0.0 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1.0, -1.0, 0.0 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1.0, -1.0, 0.0 }, .uv = .{ 1, 0 } }, +}; diff --git a/src/core/examples/sysgpu/pixel-post-process/shader.wgsl b/src/core/examples/sysgpu/pixel-post-process/shader.wgsl new file mode 100644 index 00000000..325391cd --- /dev/null +++ b/src/core/examples/sysgpu/pixel-post-process/shader.wgsl @@ -0,0 +1,27 @@ +@group(0) @binding(0) var ubo: mat4x4; + +struct VertexOut { + @builtin(position) position_clip: vec4, + @location(0) normal: vec3, + @location(1) uv: vec2, +} + +@vertex fn vertex_main( + @location(0) position: vec3, + @location(1) normal: vec3, + @location(2) uv: vec2 +) -> VertexOut { + var output: VertexOut; + output.position_clip = vec4(position, 1) * ubo; + output.normal = (vec4(normal, 0) * ubo).xyz; + output.uv = uv; + return output; +} + +@fragment fn frag_main( + @location(0) normal: vec3, + @location(1) uv: vec2, +) -> @location(0) vec4 { + var color = floor((uv * 0.5 + 0.25) * 32) / 32; + return vec4(color, 1, 1); +} \ No newline at end of file diff --git a/src/core/examples/sysgpu/procedural-primitives/main.zig b/src/core/examples/sysgpu/procedural-primitives/main.zig new file mode 100644 index 00000000..f0c51f2f --- /dev/null +++ b/src/core/examples/sysgpu/procedural-primitives/main.zig @@ -0,0 +1,66 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const renderer = @import("renderer.zig"); + +pub const App = @This(); + +pub const mach_core_options = core.ComptimeOptions{ + .use_wgpu = false, + .use_sysgpu = true, +}; +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +title_timer: core.Timer, + +pub fn init(app: *App) !void { + try core.init(.{ .required_limits = gpu.Limits{ + .max_vertex_buffers = 1, + .max_vertex_attributes = 2, + .max_bind_groups = 1, + .max_uniform_buffers_per_shader_stage = 1, + .max_uniform_buffer_binding_size = 16 * 1 * @sizeOf(f32), + } }); + + const allocator = gpa.allocator(); + const timer = try core.Timer.start(); + try renderer.init(allocator, timer); + app.* = .{ .title_timer = try core.Timer.start() }; +} + +pub fn deinit(app: *App) void { + _ = app; + defer _ = gpa.deinit(); + defer core.deinit(); + defer renderer.deinit(); +} + +pub fn update(app: *App) !bool { + var iter = core.pollEvents(); + while (iter.next()) |event| { + switch (event) { + .key_press => |ev| { + if (ev.key == .space) return true; + if (ev.key == .right) { + renderer.curr_primitive_index += 1; + renderer.curr_primitive_index %= 7; + } + }, + .close => return true, + else => {}, + } + } + + renderer.update(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Procedural Primitives [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} diff --git a/src/core/examples/sysgpu/procedural-primitives/procedural-primitives.zig b/src/core/examples/sysgpu/procedural-primitives/procedural-primitives.zig new file mode 100644 index 00000000..ae1c74ab --- /dev/null +++ b/src/core/examples/sysgpu/procedural-primitives/procedural-primitives.zig @@ -0,0 +1,336 @@ +const std = @import("std"); +const zmath = @import("zmath"); + +const PI = 3.1415927410125732421875; + +pub const F32x3 = @Vector(3, f32); +pub const F32x4 = @Vector(4, f32); +pub const VertexData = struct { + position: F32x3, + normal: F32x3, +}; + +pub const PrimitiveType = enum(u4) { none, triangle, quad, plane, circle, uv_sphere, ico_sphere, cylinder, cone, torus }; + +pub const Primitive = struct { + vertex_data: std.ArrayList(VertexData), + vertex_count: u32, + index_data: std.ArrayList(u32), + index_count: u32, + type: PrimitiveType = .none, +}; + +// 2D Primitives +pub fn createTrianglePrimitive(allocator: std.mem.Allocator, size: f32) !Primitive { + const vertex_count = 3; + const index_count = 3; + var vertex_data = try std.ArrayList(VertexData).initCapacity(allocator, vertex_count); + + const edge = size / 2.0; + + vertex_data.appendSliceAssumeCapacity(&[vertex_count]VertexData{ + VertexData{ .position = F32x3{ -edge, -edge, 0.0 }, .normal = F32x3{ -edge, -edge, 0.0 } }, + VertexData{ .position = F32x3{ edge, -edge, 0.0 }, .normal = F32x3{ edge, -edge, 0.0 } }, + VertexData{ .position = F32x3{ 0.0, edge, 0.0 }, .normal = F32x3{ 0.0, edge, 0.0 } }, + }); + + var index_data = try std.ArrayList(u32).initCapacity(allocator, index_count); + index_data.appendSliceAssumeCapacity(&[index_count]u32{ 0, 1, 2 }); + + return Primitive{ .vertex_data = vertex_data, .vertex_count = 3, .index_data = index_data, .index_count = 3, .type = .triangle }; +} + +pub fn createQuadPrimitive(allocator: std.mem.Allocator, size: f32) !Primitive { + const vertex_count = 4; + const index_count = 6; + var vertex_data = try std.ArrayList(VertexData).initCapacity(allocator, vertex_count); + + const edge = size / 2.0; + + vertex_data.appendSliceAssumeCapacity(&[vertex_count]VertexData{ + VertexData{ .position = F32x3{ -edge, -edge, 0.0 }, .normal = F32x3{ -edge, -edge, 0.0 } }, + VertexData{ .position = F32x3{ edge, -edge, 0.0 }, .normal = F32x3{ edge, -edge, 0.0 } }, + VertexData{ .position = F32x3{ -edge, edge, 0.0 }, .normal = F32x3{ -edge, edge, 0.0 } }, + VertexData{ .position = F32x3{ edge, edge, 0.0 }, .normal = F32x3{ edge, edge, 0.0 } }, + }); + + var index_data = try std.ArrayList(u32).initCapacity(allocator, index_count); + index_data.appendSliceAssumeCapacity(&[index_count]u32{ + 0, 1, 2, + 1, 3, 2, + }); + + return Primitive{ .vertex_data = vertex_data, .vertex_count = 4, .index_data = index_data, .index_count = 6, .type = .quad }; +} + +pub fn createPlanePrimitive(allocator: std.mem.Allocator, x_subdivision: u32, y_subdivision: u32, size: f32) !Primitive { + const x_num_vertices = x_subdivision + 1; + const y_num_vertices = y_subdivision + 1; + const vertex_count = x_num_vertices * y_num_vertices; + var vertex_data = try std.ArrayList(VertexData).initCapacity(allocator, vertex_count); + + const vertices_distance_y = (size / @as(f32, @floatFromInt(y_subdivision))); + const vertices_distance_x = (size / @as(f32, @floatFromInt(x_subdivision))); + var y: u32 = 0; + while (y < y_num_vertices) : (y += 1) { + var x: u32 = 0; + const pos_y = (-size / 2.0) + @as(f32, @floatFromInt(y)) * vertices_distance_y; + while (x < x_num_vertices) : (x += 1) { + const pos_x = (-size / 2.0) + @as(f32, @floatFromInt(x)) * vertices_distance_x; + vertex_data.appendAssumeCapacity(VertexData{ .position = F32x3{ pos_x, pos_y, 0.0 }, .normal = F32x3{ pos_x, pos_y, 0.0 } }); + } + } + + const index_count = x_subdivision * y_subdivision * 2 * 3; + var index_data = try std.ArrayList(u32).initCapacity(allocator, index_count); + + y = 0; + while (y < y_subdivision) : (y += 1) { + var x: u32 = 0; + while (x < x_subdivision) : (x += 1) { + // First Triangle of Quad + index_data.appendAssumeCapacity(x + y * y_num_vertices); + index_data.appendAssumeCapacity(x + 1 + y * y_num_vertices); + index_data.appendAssumeCapacity(x + (y + 1) * y_num_vertices); + + // Second Triangle of Quad + index_data.appendAssumeCapacity(x + 1 + y * y_num_vertices); + index_data.appendAssumeCapacity(x + (y + 1) * y_num_vertices + 1); + index_data.appendAssumeCapacity(x + (y + 1) * y_num_vertices); + } + } + + return Primitive{ .vertex_data = vertex_data, .vertex_count = vertex_count, .index_data = index_data, .index_count = index_count, .type = .plane }; +} + +pub fn createCirclePrimitive(allocator: std.mem.Allocator, vertices: u32, radius: f32) !Primitive { + const vertex_count = vertices + 1; + var vertex_data = try std.ArrayList(VertexData).initCapacity(allocator, vertex_count); + + // Mid point of circle + vertex_data.appendAssumeCapacity(VertexData{ .position = F32x3{ 0, 0, 0.0 }, .normal = F32x3{ 0, 0, 0.0 } }); + + var x: u32 = 0; + const angle = 2 * PI / @as(f32, @floatFromInt(vertices)); + while (x < vertices) : (x += 1) { + const x_f = @as(f32, @floatFromInt(x)); + const pos_x = radius * zmath.cos(angle * x_f); + const pos_y = radius * zmath.sin(angle * x_f); + + vertex_data.appendAssumeCapacity(VertexData{ .position = F32x3{ pos_x, pos_y, 0.0 }, .normal = F32x3{ pos_x, pos_y, 0.0 } }); + } + + const index_count = (vertices + 1) * 3; + var index_data = try std.ArrayList(u32).initCapacity(allocator, index_count); + + x = 1; + while (x <= vertices) : (x += 1) { + index_data.appendAssumeCapacity(0); + index_data.appendAssumeCapacity(x); + index_data.appendAssumeCapacity(x + 1); + } + + index_data.appendAssumeCapacity(0); + index_data.appendAssumeCapacity(vertices); + index_data.appendAssumeCapacity(1); + + return Primitive{ .vertex_data = vertex_data, .vertex_count = vertex_count, .index_data = index_data, .index_count = index_count, .type = .plane }; +} + +// 3D Primitives +pub fn createCubePrimitive(allocator: std.mem.Allocator, size: f32) !Primitive { + const vertex_count = 8; + const index_count = 36; + var vertex_data = try std.ArrayList(VertexData).initCapacity(allocator, vertex_count); + + const edge = size / 2.0; + + vertex_data.appendSliceAssumeCapacity(&[vertex_count]VertexData{ + // Front positions + VertexData{ .position = F32x3{ -edge, -edge, edge }, .normal = F32x3{ -edge, -edge, edge } }, + VertexData{ .position = F32x3{ edge, -edge, edge }, .normal = F32x3{ edge, -edge, edge } }, + VertexData{ .position = F32x3{ edge, edge, edge }, .normal = F32x3{ edge, edge, edge } }, + VertexData{ .position = F32x3{ -edge, edge, edge }, .normal = F32x3{ -edge, edge, edge } }, + // Back positions + VertexData{ .position = F32x3{ -edge, -edge, -edge }, .normal = F32x3{ -edge, -edge, -edge } }, + VertexData{ .position = F32x3{ edge, -edge, -edge }, .normal = F32x3{ edge, -edge, -edge } }, + VertexData{ .position = F32x3{ edge, edge, -edge }, .normal = F32x3{ edge, edge, -edge } }, + VertexData{ .position = F32x3{ -edge, edge, -edge }, .normal = F32x3{ -edge, edge, -edge } }, + }); + + var index_data = try std.ArrayList(u32).initCapacity(allocator, index_count); + + index_data.appendSliceAssumeCapacity(&[index_count]u32{ + // front quad + 0, 1, 2, + 2, 3, 0, + // right quad + 1, 5, 6, + 6, 2, 1, + // back quad + 7, 6, 5, + 5, 4, 7, + // left quad + 4, 0, 3, + 3, 7, 4, + // bottom quad + 4, 5, 1, + 1, 0, 4, + // top quad + 3, 2, 6, + 6, 7, 3, + }); + + return Primitive{ .vertex_data = vertex_data, .vertex_count = vertex_count, .index_data = index_data, .index_count = index_count, .type = .quad }; +} + +const VertexDataMAL = std.MultiArrayList(VertexData); + +pub fn createCylinderPrimitive(allocator: std.mem.Allocator, radius: f32, height: f32, num_sides: u32) !Primitive { + const alloc_amt_vert: u32 = num_sides * 2 + 2; + const alloc_amt_idx: u32 = num_sides * 12; + + var vertex_data = VertexDataMAL{}; + try vertex_data.ensureTotalCapacity(allocator, alloc_amt_vert); + defer vertex_data.deinit(allocator); + + var out_vertex_data = try std.ArrayList(VertexData).initCapacity(allocator, alloc_amt_vert); + var index_data = try std.ArrayList(u32).initCapacity(allocator, alloc_amt_idx); + + vertex_data.appendAssumeCapacity(VertexData{ .position = F32x3{ 0.0, (height / 2.0), 0.0 }, .normal = undefined }); + vertex_data.appendAssumeCapacity(VertexData{ .position = F32x3{ 0.0, -(height / 2.0), 0.0 }, .normal = undefined }); + + const angle = 2.0 * PI / @as(f32, @floatFromInt(num_sides)); + + for (1..num_sides + 1) |i| { + const float_i = @as(f32, @floatFromInt(i)); + + const x: f32 = radius * zmath.sin(angle * float_i); + const y: f32 = radius * zmath.cos(angle * float_i); + + vertex_data.appendAssumeCapacity(VertexData{ .position = F32x3{ x, (height / 2.0), y }, .normal = undefined }); + vertex_data.appendAssumeCapacity(VertexData{ .position = F32x3{ x, -(height / 2.0), y }, .normal = undefined }); + } + + var group1: u32 = 1; + var group2: u32 = 3; + + for (0..num_sides) |_| { + if (group2 >= num_sides * 2) group2 = 1; + index_data.appendSliceAssumeCapacity(&[_]u32{ + 0, group1 + 1, group2 + 1, + group1 + 1, group1 + 2, group2 + 1, + group1 + 2, group2 + 2, group2 + 1, + group2 + 2, group1 + 2, 1, + }); + group1 += 2; + group2 += 2; + } + + { + var i: u32 = 0; + while (i < alloc_amt_idx) : (i += 3) { + const indexA: u32 = index_data.items[i]; + const indexB: u32 = index_data.items[i + 1]; + const indexC: u32 = index_data.items[i + 2]; + + const vert1: F32x4 = F32x4{ vertex_data.get(indexA).position[0], vertex_data.get(indexA).position[1], vertex_data.get(indexA).position[2], 1.0 }; + const vert2: F32x4 = F32x4{ vertex_data.get(indexB).position[0], vertex_data.get(indexB).position[1], vertex_data.get(indexB).position[2], 1.0 }; + const vert3: F32x4 = F32x4{ vertex_data.get(indexC).position[0], vertex_data.get(indexC).position[1], vertex_data.get(indexC).position[2], 1.0 }; + + const edgeAB: F32x4 = vert2 - vert1; + const edgeAC: F32x4 = vert3 - vert1; + + const cross = zmath.cross3(edgeAB, edgeAC); + + vertex_data.items(.normal)[indexA][0] += cross[0]; + vertex_data.items(.normal)[indexA][1] += cross[1]; + vertex_data.items(.normal)[indexA][2] += cross[2]; + vertex_data.items(.normal)[indexB][0] += cross[0]; + vertex_data.items(.normal)[indexB][1] += cross[1]; + vertex_data.items(.normal)[indexB][2] += cross[2]; + vertex_data.items(.normal)[indexC][0] += cross[0]; + vertex_data.items(.normal)[indexC][1] += cross[1]; + vertex_data.items(.normal)[indexC][2] += cross[2]; + } + } + + for (vertex_data.items(.position), vertex_data.items(.normal)) |pos, nor| { + out_vertex_data.appendAssumeCapacity(VertexData{ .position = pos, .normal = nor }); + } + + return Primitive{ .vertex_data = out_vertex_data, .vertex_count = alloc_amt_vert, .index_data = index_data, .index_count = alloc_amt_idx, .type = .cylinder }; +} + +pub fn createConePrimitive(allocator: std.mem.Allocator, radius: f32, height: f32, num_sides: u32) !Primitive { + const alloc_amt_vert: u32 = num_sides + 2; + const alloc_amt_idx: u32 = num_sides * 6; + + var vertex_data = VertexDataMAL{}; + try vertex_data.ensureTotalCapacity(allocator, alloc_amt_vert); + defer vertex_data.deinit(allocator); + + var out_vertex_data = try std.ArrayList(VertexData).initCapacity(allocator, alloc_amt_vert); + var index_data = try std.ArrayList(u32).initCapacity(allocator, alloc_amt_idx); + + vertex_data.appendAssumeCapacity(VertexData{ .position = F32x3{ 0.0, (height / 2.0), 0.0 }, .normal = undefined }); + vertex_data.appendAssumeCapacity(VertexData{ .position = F32x3{ 0.0, -(height / 2.0), 0.0 }, .normal = undefined }); + + const angle = 2.0 * PI / @as(f32, @floatFromInt(num_sides)); + + for (1..num_sides + 1) |i| { + const float_i = @as(f32, @floatFromInt(i)); + + const x: f32 = radius * zmath.sin(angle * float_i); + const y: f32 = radius * zmath.cos(angle * float_i); + + vertex_data.appendAssumeCapacity(VertexData{ .position = F32x3{ x, -(height / 2.0), y }, .normal = undefined }); + } + + var group1: u32 = 1; + var group2: u32 = 2; + + for (0..num_sides) |_| { + if (group2 >= num_sides + 1) group2 = 1; + index_data.appendSliceAssumeCapacity(&[_]u32{ + 0, group1 + 1, group2 + 1, + group2 + 1, group1 + 1, 1, + }); + group1 += 1; + group2 += 1; + } + + { + var i: u32 = 0; + while (i < alloc_amt_idx) : (i += 3) { + const indexA: u32 = index_data.items[i]; + const indexB: u32 = index_data.items[i + 1]; + const indexC: u32 = index_data.items[i + 2]; + + const vert1: F32x4 = F32x4{ vertex_data.get(indexA).position[0], vertex_data.get(indexA).position[1], vertex_data.get(indexA).position[2], 1.0 }; + const vert2: F32x4 = F32x4{ vertex_data.get(indexB).position[0], vertex_data.get(indexB).position[1], vertex_data.get(indexB).position[2], 1.0 }; + const vert3: F32x4 = F32x4{ vertex_data.get(indexC).position[0], vertex_data.get(indexC).position[1], vertex_data.get(indexC).position[2], 1.0 }; + + const edgeAB: F32x4 = vert2 - vert1; + const edgeAC: F32x4 = vert3 - vert1; + + const cross = zmath.cross3(edgeAB, edgeAC); + + vertex_data.items(.normal)[indexA][0] += cross[0]; + vertex_data.items(.normal)[indexA][1] += cross[1]; + vertex_data.items(.normal)[indexA][2] += cross[2]; + vertex_data.items(.normal)[indexB][0] += cross[0]; + vertex_data.items(.normal)[indexB][1] += cross[1]; + vertex_data.items(.normal)[indexB][2] += cross[2]; + vertex_data.items(.normal)[indexC][0] += cross[0]; + vertex_data.items(.normal)[indexC][1] += cross[1]; + vertex_data.items(.normal)[indexC][2] += cross[2]; + } + } + + for (vertex_data.items(.position), vertex_data.items(.normal)) |pos, nor| { + out_vertex_data.appendAssumeCapacity(VertexData{ .position = pos, .normal = nor }); + } + + return Primitive{ .vertex_data = out_vertex_data, .vertex_count = alloc_amt_vert, .index_data = index_data, .index_count = alloc_amt_idx, .type = .cone }; +} diff --git a/src/core/examples/sysgpu/procedural-primitives/renderer.zig b/src/core/examples/sysgpu/procedural-primitives/renderer.zig new file mode 100644 index 00000000..0ad0d558 --- /dev/null +++ b/src/core/examples/sysgpu/procedural-primitives/renderer.zig @@ -0,0 +1,325 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const zm = @import("zmath"); +const primitives = @import("procedural-primitives.zig"); +const Primitive = primitives.Primitive; +const VertexData = primitives.VertexData; + +pub const Renderer = @This(); + +var queue: *gpu.Queue = undefined; +var pipeline: *gpu.RenderPipeline = undefined; +var app_timer: core.Timer = undefined; +var depth_texture: *gpu.Texture = undefined; +var depth_texture_view: *gpu.TextureView = undefined; + +const PrimitiveRenderData = struct { + vertex_buffer: *gpu.Buffer, + index_buffer: *gpu.Buffer, + vertex_count: u32, + index_count: u32, +}; + +const UniformBufferObject = struct { + mvp_matrix: zm.Mat, +}; +var uniform_buffer: *gpu.Buffer = undefined; +var bind_group: *gpu.BindGroup = undefined; + +var primitives_data: [7]PrimitiveRenderData = undefined; + +pub var curr_primitive_index: u4 = 0; + +pub fn init(allocator: std.mem.Allocator, timer: core.Timer) !void { + queue = core.queue; + app_timer = timer; + + { + const triangle_primitive = try primitives.createTrianglePrimitive(allocator, 1); + primitives_data[0] = PrimitiveRenderData{ .vertex_buffer = createVertexBuffer(triangle_primitive), .index_buffer = createIndexBuffer(triangle_primitive), .vertex_count = triangle_primitive.vertex_count, .index_count = triangle_primitive.index_count }; + defer triangle_primitive.vertex_data.deinit(); + defer triangle_primitive.index_data.deinit(); + } + + { + const quad_primitive = try primitives.createQuadPrimitive(allocator, 1.4); + primitives_data[1] = PrimitiveRenderData{ .vertex_buffer = createVertexBuffer(quad_primitive), .index_buffer = createIndexBuffer(quad_primitive), .vertex_count = quad_primitive.vertex_count, .index_count = quad_primitive.index_count }; + defer quad_primitive.vertex_data.deinit(); + defer quad_primitive.index_data.deinit(); + } + + { + const plane_primitive = try primitives.createPlanePrimitive(allocator, 1000, 1000, 1.5); + primitives_data[2] = PrimitiveRenderData{ .vertex_buffer = createVertexBuffer(plane_primitive), .index_buffer = createIndexBuffer(plane_primitive), .vertex_count = plane_primitive.vertex_count, .index_count = plane_primitive.index_count }; + defer plane_primitive.vertex_data.deinit(); + defer plane_primitive.index_data.deinit(); + } + + { + const circle_primitive = try primitives.createCirclePrimitive(allocator, 64, 1); + primitives_data[3] = PrimitiveRenderData{ .vertex_buffer = createVertexBuffer(circle_primitive), .index_buffer = createIndexBuffer(circle_primitive), .vertex_count = circle_primitive.vertex_count, .index_count = circle_primitive.index_count }; + defer circle_primitive.vertex_data.deinit(); + defer circle_primitive.index_data.deinit(); + } + + { + const cube_primitive = try primitives.createCubePrimitive(allocator, 0.5); + primitives_data[4] = PrimitiveRenderData{ .vertex_buffer = createVertexBuffer(cube_primitive), .index_buffer = createIndexBuffer(cube_primitive), .vertex_count = cube_primitive.vertex_count, .index_count = cube_primitive.index_count }; + defer cube_primitive.vertex_data.deinit(); + defer cube_primitive.index_data.deinit(); + } + + { + const cylinder_primitive = try primitives.createCylinderPrimitive(allocator, 1.0, 1.0, 6); + primitives_data[5] = PrimitiveRenderData{ .vertex_buffer = createVertexBuffer(cylinder_primitive), .index_buffer = createIndexBuffer(cylinder_primitive), .vertex_count = cylinder_primitive.vertex_count, .index_count = cylinder_primitive.index_count }; + defer cylinder_primitive.vertex_data.deinit(); + defer cylinder_primitive.index_data.deinit(); + } + + { + const cone_primitive = try primitives.createConePrimitive(allocator, 0.7, 1.0, 15); + primitives_data[6] = PrimitiveRenderData{ .vertex_buffer = createVertexBuffer(cone_primitive), .index_buffer = createIndexBuffer(cone_primitive), .vertex_count = cone_primitive.vertex_count, .index_count = cone_primitive.index_count }; + defer cone_primitive.vertex_data.deinit(); + defer cone_primitive.index_data.deinit(); + } + var bind_group_layout = createBindGroupLayout(); + defer bind_group_layout.release(); + + createBindBuffer(bind_group_layout); + + createDepthTexture(); + + var shader = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); + defer shader.release(); + + pipeline = createPipeline(shader, bind_group_layout); +} + +fn createVertexBuffer(primitive: Primitive) *gpu.Buffer { + const vertex_buffer_descriptor = gpu.Buffer.Descriptor{ + .size = primitive.vertex_count * @sizeOf(VertexData), + .usage = .{ .vertex = true, .copy_dst = true }, + .mapped_at_creation = .false, + }; + + const vertex_buffer = core.device.createBuffer(&vertex_buffer_descriptor); + queue.writeBuffer(vertex_buffer, 0, primitive.vertex_data.items[0..]); + + return vertex_buffer; +} + +fn createIndexBuffer(primitive: Primitive) *gpu.Buffer { + const index_buffer_descriptor = gpu.Buffer.Descriptor{ + .size = primitive.index_count * @sizeOf(u32), + .usage = .{ .index = true, .copy_dst = true }, + .mapped_at_creation = .false, + }; + const index_buffer = core.device.createBuffer(&index_buffer_descriptor); + queue.writeBuffer(index_buffer, 0, primitive.index_data.items[0..]); + + return index_buffer; +} + +fn createBindGroupLayout() *gpu.BindGroupLayout { + const bgle = gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true, .fragment = false }, .uniform, true, 0); + return core.device.createBindGroupLayout( + &gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &.{bgle}, + }), + ); +} + +fn createBindBuffer(bind_group_layout: *gpu.BindGroupLayout) void { + uniform_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = @sizeOf(UniformBufferObject), + .mapped_at_creation = .false, + }); + + bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bind_group_layout, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, uniform_buffer, 0, @sizeOf(UniformBufferObject), @sizeOf(UniformBufferObject)), + }, + }), + ); +} + +fn createDepthTexture() void { + depth_texture = core.device.createTexture(&gpu.Texture.Descriptor{ + .usage = .{ .render_attachment = true }, + .size = .{ .width = core.descriptor.width, .height = core.descriptor.height }, + .format = .depth24_plus, + }); + + depth_texture_view = depth_texture.createView(&gpu.TextureView.Descriptor{ + .format = .depth24_plus, + .dimension = .dimension_2d, + .array_layer_count = 1, + .mip_level_count = 1, + }); +} + +fn createPipeline(shader_module: *gpu.ShaderModule, bind_group_layout: *gpu.BindGroupLayout) *gpu.RenderPipeline { + const vertex_attributes = [_]gpu.VertexAttribute{ + .{ .format = .float32x3, .shader_location = 0, .offset = 0 }, + .{ .format = .float32x3, .shader_location = 1, .offset = @sizeOf(primitives.F32x3) }, + }; + + const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{ + .array_stride = @sizeOf(VertexData), + .step_mode = .vertex, + .attributes = &vertex_attributes, + }); + + const vertex_pipeline_state = gpu.VertexState.init(.{ .module = shader_module, .entry_point = "vertex_main", .buffers = &.{vertex_buffer_layout} }); + + const primitive_pipeline_state = gpu.PrimitiveState{ + .topology = .triangle_list, + .front_face = .ccw, + .cull_mode = .back, + }; + + // Fragment Pipeline State + const blend = gpu.BlendState{ + .color = gpu.BlendComponent{ .operation = .add, .src_factor = .src_alpha, .dst_factor = .one_minus_src_alpha }, + .alpha = gpu.BlendComponent{ .operation = .add, .src_factor = .zero, .dst_factor = .one }, + }; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment_pipeline_state = gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + + const depth_stencil_state = gpu.DepthStencilState{ + .format = .depth24_plus, + .depth_write_enabled = .true, + .depth_compare = .less, + }; + + const multi_sample_state = gpu.MultisampleState{ + .count = 1, + .mask = 0xFFFFFFFF, + .alpha_to_coverage_enabled = .false, + }; + + const bind_group_layouts = [_]*gpu.BindGroupLayout{bind_group_layout}; + // Pipeline Layout + const pipeline_layout_descriptor = gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &bind_group_layouts, + }); + const pipeline_layout = core.device.createPipelineLayout(&pipeline_layout_descriptor); + defer pipeline_layout.release(); + + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .label = "Main Pipeline", + .layout = pipeline_layout, + .vertex = vertex_pipeline_state, + .primitive = primitive_pipeline_state, + .depth_stencil = &depth_stencil_state, + .multisample = multi_sample_state, + .fragment = &fragment_pipeline_state, + }; + + return core.device.createRenderPipeline(&pipeline_descriptor); +} + +pub const F32x1 = @Vector(1, f32); + +pub fn update() void { + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = gpu.Color{ .r = 0.2, .g = 0.2, .b = 0.2, .a = 1.0 }, + .load_op = .clear, + .store_op = .store, + }; + + const depth_stencil_attachment = gpu.RenderPassDepthStencilAttachment{ + .view = depth_texture_view, + .depth_load_op = .clear, + .depth_store_op = .store, + .depth_clear_value = 1.0, + }; + + const encoder = core.device.createCommandEncoder(null); + const render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + .depth_stencil_attachment = &depth_stencil_attachment, + }); + + if (curr_primitive_index >= 4) { + const time = app_timer.read() / 5; + const model = zm.mul(zm.rotationX(time * (std.math.pi / 2.0)), zm.rotationZ(time * (std.math.pi / 2.0))); + const view = zm.lookAtRh( + zm.Vec{ 0, 4, 2, 1 }, + zm.Vec{ 0, 0, 0, 1 }, + zm.Vec{ 0, 0, 1, 0 }, + ); + const proj = zm.perspectiveFovRh( + (std.math.pi / 4.0), + @as(f32, @floatFromInt(core.descriptor.width)) / @as(f32, @floatFromInt(core.descriptor.height)), + 0.1, + 10, + ); + + const mvp = zm.mul(zm.mul(model, view), proj); + + const ubo = UniformBufferObject{ + .mvp_matrix = zm.transpose(mvp), + }; + encoder.writeBuffer(uniform_buffer, 0, &[_]UniformBufferObject{ubo}); + } else { + const ubo = UniformBufferObject{ + .mvp_matrix = zm.identity(), + }; + encoder.writeBuffer(uniform_buffer, 0, &[_]UniformBufferObject{ubo}); + } + + const pass = encoder.beginRenderPass(&render_pass_info); + + pass.setPipeline(pipeline); + + const vertex_buffer = primitives_data[curr_primitive_index].vertex_buffer; + const vertex_count = primitives_data[curr_primitive_index].vertex_count; + pass.setVertexBuffer(0, vertex_buffer, 0, @sizeOf(VertexData) * vertex_count); + + pass.setBindGroup(0, bind_group, &.{0}); + + const index_buffer = primitives_data[curr_primitive_index].index_buffer; + const index_count = primitives_data[curr_primitive_index].index_count; + pass.setIndexBuffer(index_buffer, .uint32, 0, @sizeOf(u32) * index_count); + pass.drawIndexed(index_count, 1, 0, 0, 0); + + pass.end(); + pass.release(); + + var command = encoder.finish(null); + encoder.release(); + + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); +} + +pub fn deinit() void { + var i: u4 = 0; + while (i < 7) : (i += 1) { + primitives_data[i].vertex_buffer.release(); + primitives_data[i].index_buffer.release(); + } + + bind_group.release(); + uniform_buffer.release(); + depth_texture.release(); + depth_texture_view.release(); + pipeline.release(); +} diff --git a/src/core/examples/sysgpu/procedural-primitives/shader.wgsl b/src/core/examples/sysgpu/procedural-primitives/shader.wgsl new file mode 100644 index 00000000..18606b69 --- /dev/null +++ b/src/core/examples/sysgpu/procedural-primitives/shader.wgsl @@ -0,0 +1,32 @@ +struct Uniforms { + mvp_matrix : mat4x4, +}; + +@binding(0) @group(0) var ubo : Uniforms; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) normal: vec3, +}; + +@vertex fn vertex_main( + // TODO - struct input + @location(0) position: vec3, + @location(1) normal: vec3, +) -> VertexOutput { + var out: VertexOutput; + out.position = vec4(position, 1.0) * ubo.mvp_matrix; + out.normal = normal; + return out; +} + +struct FragmentOutput { + @location(0) pixel_color: vec4 +}; + +@fragment fn frag_main(in: VertexOutput) -> FragmentOutput { + var out : FragmentOutput; + + out.pixel_color = vec4((in.normal + 1) / 2, 1.0); + return out; +} \ No newline at end of file diff --git a/src/core/examples/sysgpu/rgb-quad/main.zig b/src/core/examples/sysgpu/rgb-quad/main.zig new file mode 100644 index 00000000..791b53f7 --- /dev/null +++ b/src/core/examples/sysgpu/rgb-quad/main.zig @@ -0,0 +1,143 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; + +const Vertex = extern struct { + pos: @Vector(2, f32), + col: @Vector(3, f32), +}; +const vertices = [_]Vertex{ + .{ .pos = .{ -0.5, -0.5 }, .col = .{ 1, 0, 0 } }, + .{ .pos = .{ 0.5, -0.5 }, .col = .{ 0, 1, 0 } }, + .{ .pos = .{ 0.5, 0.5 }, .col = .{ 0, 0, 1 } }, + .{ .pos = .{ -0.5, 0.5 }, .col = .{ 1, 1, 1 } }, +}; +const index_data = [_]u32{ 0, 1, 2, 2, 3, 0 }; + +pub const mach_core_options = core.ComptimeOptions{ + .use_wgpu = false, + .use_sysgpu = true, +}; + +pub const App = @This(); + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +title_timer: core.Timer, +pipeline: *gpu.RenderPipeline, +vertex_buffer: *gpu.Buffer, +index_buffer: *gpu.Buffer, + +pub fn init(app: *App) !void { + try core.init(.{}); + + const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); + defer shader_module.release(); + + const vertex_attributes = [_]gpu.VertexAttribute{ + .{ .format = .float32x2, .offset = @offsetOf(Vertex, "pos"), .shader_location = 0 }, + .{ .format = .float32x3, .offset = @offsetOf(Vertex, "col"), .shader_location = 1 }, + }; + const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{ + .array_stride = @sizeOf(Vertex), + .step_mode = .vertex, + .attributes = &vertex_attributes, + }); + + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &.{}, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + + const pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{})); + defer pipeline_layout.release(); + + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + .layout = pipeline_layout, + .vertex = gpu.VertexState.init(.{ + .module = shader_module, + .entry_point = "vertex_main", + .buffers = &.{vertex_buffer_layout}, + }), + .primitive = .{ .cull_mode = .back }, + }; + + const vertex_buffer = core.device.createBuffer(&.{ + .usage = .{ .vertex = true }, + .size = @sizeOf(Vertex) * vertices.len, + .mapped_at_creation = .true, + }); + const vertex_mapped = vertex_buffer.getMappedRange(Vertex, 0, vertices.len); + @memcpy(vertex_mapped.?, vertices[0..]); + vertex_buffer.unmap(); + + const index_buffer = core.device.createBuffer(&.{ + .usage = .{ .index = true }, + .size = @sizeOf(u32) * index_data.len, + .mapped_at_creation = .true, + }); + const index_mapped = index_buffer.getMappedRange(u32, 0, index_data.len); + @memcpy(index_mapped.?, index_data[0..]); + index_buffer.unmap(); + + app.title_timer = try core.Timer.start(); + app.pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + app.vertex_buffer = vertex_buffer; + app.index_buffer = index_buffer; +} + +pub fn deinit(app: *App) void { + app.vertex_buffer.release(); + app.index_buffer.release(); + app.pipeline.release(); + core.deinit(); + _ = gpa.deinit(); +} + +pub fn update(app: *App) !bool { + var iter = core.pollEvents(); + while (iter.next()) |event| if (event == .close) return true; + + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const encoder = core.device.createCommandEncoder(null); + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = .{ .r = 0, .g = 0, .b = 0, .a = 1 }, + .load_op = .clear, + .store_op = .store, + }; + const render_pass_info = gpu.RenderPassDescriptor.init(.{ .color_attachments = &.{color_attachment} }); + + const pass = encoder.beginRenderPass(&render_pass_info); + pass.setPipeline(app.pipeline); + pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len); + pass.setIndexBuffer(app.index_buffer, .uint32, 0, @sizeOf(u32) * index_data.len); + pass.drawIndexed(index_data.len, 1, 0, 0, 0); + pass.end(); + pass.release(); + + var command = encoder.finish(null); + encoder.release(); + core.queue.submit(&.{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("RGB Quad [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} diff --git a/src/core/examples/sysgpu/rgb-quad/shader.wgsl b/src/core/examples/sysgpu/rgb-quad/shader.wgsl new file mode 100644 index 00000000..85f687bc --- /dev/null +++ b/src/core/examples/sysgpu/rgb-quad/shader.wgsl @@ -0,0 +1,15 @@ +struct Output { + @builtin(position) pos: vec4, + @location(0) color: vec3, +}; + +@vertex fn vertex_main(@location(0) pos: vec2, @location(1) color: vec3) -> Output { + var output: Output; + output.pos = vec4(pos, 0, 1); + output.color = color; + return output; +} + +@fragment fn frag_main(@location(0) color: vec3) -> @location(0) vec4 { + return vec4(color, 1); +} \ No newline at end of file diff --git a/src/core/examples/sysgpu/rotating-cube/cube_mesh.zig b/src/core/examples/sysgpu/rotating-cube/cube_mesh.zig new file mode 100644 index 00000000..f26c75ac --- /dev/null +++ b/src/core/examples/sysgpu/rotating-cube/cube_mesh.zig @@ -0,0 +1,49 @@ +pub const Vertex = extern struct { + pos: @Vector(4, f32), + col: @Vector(4, f32), + uv: @Vector(2, f32), +}; + +pub const vertices = [_]Vertex{ + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, +}; diff --git a/src/core/examples/sysgpu/rotating-cube/main.zig b/src/core/examples/sysgpu/rotating-cube/main.zig new file mode 100644 index 00000000..42e7a561 --- /dev/null +++ b/src/core/examples/sysgpu/rotating-cube/main.zig @@ -0,0 +1,197 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const zm = @import("zmath"); +const Vertex = @import("cube_mesh.zig").Vertex; +const vertices = @import("cube_mesh.zig").vertices; + +pub const App = @This(); + +pub const mach_core_options = core.ComptimeOptions{ + .use_wgpu = false, + .use_sysgpu = true, +}; + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; +const UniformBufferObject = struct { + mat: zm.Mat, +}; + +title_timer: core.Timer, +timer: core.Timer, +pipeline: *gpu.RenderPipeline, +vertex_buffer: *gpu.Buffer, +uniform_buffer: *gpu.Buffer, +bind_group: *gpu.BindGroup, + +pub fn init(app: *App) !void { + try core.init(.{}); + + const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); + + const vertex_attributes = [_]gpu.VertexAttribute{ + .{ .format = .float32x4, .offset = @offsetOf(Vertex, "pos"), .shader_location = 0 }, + .{ .format = .float32x2, .offset = @offsetOf(Vertex, "uv"), .shader_location = 1 }, + }; + const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{ + .array_stride = @sizeOf(Vertex), + .step_mode = .vertex, + .attributes = &vertex_attributes, + }); + + const blend = gpu.BlendState{}; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + + const bgle = gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, true, 0); + const bgl = core.device.createBindGroupLayout( + &gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &.{bgle}, + }), + ); + + const bind_group_layouts = [_]*gpu.BindGroupLayout{bgl}; + const pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &bind_group_layouts, + })); + + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + .layout = pipeline_layout, + .vertex = gpu.VertexState.init(.{ + .module = shader_module, + .entry_point = "vertex_main", + .buffers = &.{vertex_buffer_layout}, + }), + .primitive = .{ + .cull_mode = .back, + }, + }; + + const vertex_buffer = core.device.createBuffer(&.{ + .usage = .{ .vertex = true }, + .size = @sizeOf(Vertex) * vertices.len, + .mapped_at_creation = .true, + }); + const vertex_mapped = vertex_buffer.getMappedRange(Vertex, 0, vertices.len); + @memcpy(vertex_mapped.?, vertices[0..]); + vertex_buffer.unmap(); + + const uniform_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = @sizeOf(UniformBufferObject), + .mapped_at_creation = .false, + }); + const bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bgl, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, uniform_buffer, 0, @sizeOf(UniformBufferObject), @sizeOf(UniformBufferObject)), + }, + }), + ); + + app.title_timer = try core.Timer.start(); + app.timer = try core.Timer.start(); + app.pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + app.vertex_buffer = vertex_buffer; + app.uniform_buffer = uniform_buffer; + app.bind_group = bind_group; + + shader_module.release(); + pipeline_layout.release(); + bgl.release(); +} + +pub fn deinit(app: *App) void { + defer _ = gpa.deinit(); + defer core.deinit(); + + app.vertex_buffer.release(); + app.uniform_buffer.release(); + app.bind_group.release(); + app.pipeline.release(); +} + +pub fn update(app: *App) !bool { + var iter = core.pollEvents(); + while (iter.next()) |event| { + switch (event) { + .key_press => |ev| { + if (ev.key == .space) return true; + }, + .close => return true, + else => {}, + } + } + + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = std.mem.zeroes(gpu.Color), + .load_op = .clear, + .store_op = .store, + }; + + const queue = core.queue; + const encoder = core.device.createCommandEncoder(null); + const render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + }); + + { + const time = app.timer.read(); + const model = zm.mul(zm.rotationX(time * (std.math.pi / 2.0)), zm.rotationZ(time * (std.math.pi / 2.0))); + const view = zm.lookAtRh( + zm.Vec{ 0, 4, 2, 1 }, + zm.Vec{ 0, 0, 0, 1 }, + zm.Vec{ 0, 0, 1, 0 }, + ); + const proj = zm.perspectiveFovRh( + (std.math.pi / 4.0), + @as(f32, @floatFromInt(core.descriptor.width)) / @as(f32, @floatFromInt(core.descriptor.height)), + 0.1, + 10, + ); + const mvp = zm.mul(zm.mul(model, view), proj); + const ubo = UniformBufferObject{ + .mat = zm.transpose(mvp), + }; + queue.writeBuffer(app.uniform_buffer, 0, &[_]UniformBufferObject{ubo}); + } + + const pass = encoder.beginRenderPass(&render_pass_info); + pass.setPipeline(app.pipeline); + pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len); + pass.setBindGroup(0, app.bind_group, &.{0}); + pass.draw(vertices.len, 1, 0, 0); + pass.end(); + pass.release(); + + var command = encoder.finish(null); + encoder.release(); + + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Rotating Cube [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} diff --git a/src/core/examples/sysgpu/rotating-cube/shader.wgsl b/src/core/examples/sysgpu/rotating-cube/shader.wgsl new file mode 100644 index 00000000..6b7291ee --- /dev/null +++ b/src/core/examples/sysgpu/rotating-cube/shader.wgsl @@ -0,0 +1,24 @@ +@group(0) @binding(0) var ubo : mat4x4; +struct VertexOut { + @builtin(position) position_clip : vec4, + @location(0) fragUV : vec2, + @location(1) fragPosition: vec4, +} + +@vertex fn vertex_main( + @location(0) position : vec4, + @location(1) uv: vec2 +) -> VertexOut { + var output : VertexOut; + output.position_clip = position * ubo; + output.fragUV = uv; + output.fragPosition = 0.5 * (position + vec4(1.0, 1.0, 1.0, 1.0)); + return output; +} + +@fragment fn frag_main( + @location(0) fragUV: vec2, + @location(1) fragPosition: vec4 +) -> @location(0) vec4 { + return fragPosition; +} \ No newline at end of file diff --git a/src/core/examples/sysgpu/sprite2d/main.zig b/src/core/examples/sysgpu/sprite2d/main.zig new file mode 100644 index 00000000..0bc8d6a5 --- /dev/null +++ b/src/core/examples/sysgpu/sprite2d/main.zig @@ -0,0 +1,359 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const zm = @import("zmath"); +const zigimg = @import("zigimg"); +const assets = @import("assets"); +const json = std.json; + +pub const App = @This(); + +pub const mach_core_options = core.ComptimeOptions{ + .use_wgpu = false, + .use_sysgpu = true, +}; + +const speed = 2.0 * 100.0; // pixels per second + +const Vec2 = @Vector(2, f32); + +const UniformBufferObject = struct { + mat: zm.Mat, +}; +const Sprite = extern struct { + pos: Vec2, + size: Vec2, + world_pos: Vec2, + sheet_size: Vec2, +}; +const SpriteFrames = extern struct { + up: Vec2, + down: Vec2, + left: Vec2, + right: Vec2, +}; +const JSONFrames = struct { + up: []f32, + down: []f32, + left: []f32, + right: []f32, +}; +const JSONSprite = struct { + pos: []f32, + size: []f32, + world_pos: []f32, + is_player: bool = false, + frames: JSONFrames, +}; +const SpriteSheet = struct { + width: f32, + height: f32, +}; +const JSONData = struct { + sheet: SpriteSheet, + sprites: []JSONSprite, +}; +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +title_timer: core.Timer, +timer: core.Timer, +fps_timer: core.Timer, +pipeline: *gpu.RenderPipeline, +uniform_buffer: *gpu.Buffer, +bind_group: *gpu.BindGroup, +sheet: SpriteSheet, +sprites_buffer: *gpu.Buffer, +sprites: std.ArrayList(Sprite), +sprites_frames: std.ArrayList(SpriteFrames), +player_pos: Vec2, +direction: Vec2, +player_sprite_index: usize, + +pub fn init(app: *App) !void { + try core.init(.{}); + + const allocator = gpa.allocator(); + + const sprites_file = try std.fs.cwd().openFile("../../examples/sprite2d/sprites.json", .{ .mode = .read_only }); + defer sprites_file.close(); + const file_size = (try sprites_file.stat()).size; + const buffer = try allocator.alloc(u8, file_size); + defer allocator.free(buffer); + try sprites_file.reader().readNoEof(buffer); + const root = try std.json.parseFromSlice(JSONData, allocator, buffer, .{}); + defer root.deinit(); + + app.player_pos = Vec2{ 0, 0 }; + app.direction = Vec2{ 0, 0 }; + app.sheet = root.value.sheet; + std.log.info("Sheet Dimensions: {} {}", .{ app.sheet.width, app.sheet.height }); + app.sprites = std.ArrayList(Sprite).init(allocator); + app.sprites_frames = std.ArrayList(SpriteFrames).init(allocator); + for (root.value.sprites) |sprite| { + std.log.info("Sprite World Position: {} {}", .{ sprite.world_pos[0], sprite.world_pos[1] }); + std.log.info("Sprite Texture Position: {} {}", .{ sprite.pos[0], sprite.pos[1] }); + std.log.info("Sprite Dimensions: {} {}", .{ sprite.size[0], sprite.size[1] }); + if (sprite.is_player) { + app.player_sprite_index = app.sprites.items.len; + } + try app.sprites.append(.{ + .pos = Vec2{ sprite.pos[0], sprite.pos[1] }, + .size = Vec2{ sprite.size[0], sprite.size[1] }, + .world_pos = Vec2{ sprite.world_pos[0], sprite.world_pos[1] }, + .sheet_size = Vec2{ app.sheet.width, app.sheet.height }, + }); + try app.sprites_frames.append(.{ .up = Vec2{ sprite.frames.up[0], sprite.frames.up[1] }, .down = Vec2{ sprite.frames.down[0], sprite.frames.down[1] }, .left = Vec2{ sprite.frames.left[0], sprite.frames.left[1] }, .right = Vec2{ sprite.frames.right[0], sprite.frames.right[1] } }); + } + std.log.info("Number of sprites: {}", .{app.sprites.items.len}); + + const shader_module = core.device.createShaderModuleWGSL("sprite-shader.wgsl", @embedFile("sprite-shader.wgsl")); + + const blend = gpu.BlendState{ + .color = .{ + .operation = .add, + .src_factor = .src_alpha, + .dst_factor = .one_minus_src_alpha, + }, + .alpha = .{ + .operation = .add, + .src_factor = .one, + .dst_factor = .zero, + }, + }; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + .vertex = gpu.VertexState.init(.{ + .module = shader_module, + .entry_point = "vertex_main", + }), + }; + const pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + + const sprites_buffer = core.device.createBuffer(&.{ + .usage = .{ .storage = true, .copy_dst = true }, + .size = @sizeOf(Sprite) * app.sprites.items.len, + .mapped_at_creation = .true, + }); + const sprites_mapped = sprites_buffer.getMappedRange(Sprite, 0, app.sprites.items.len); + @memcpy(sprites_mapped.?, app.sprites.items[0..]); + sprites_buffer.unmap(); + + // Create a sampler with linear filtering for smooth interpolation. + const sampler = core.device.createSampler(&.{ + .mag_filter = .linear, + .min_filter = .linear, + }); + const queue = core.queue; + var img = try zigimg.Image.fromMemory(allocator, assets.sprites_sheet_png); + defer img.deinit(); + const img_size = gpu.Extent3D{ .width = @as(u32, @intCast(img.width)), .height = @as(u32, @intCast(img.height)) }; + std.log.info("Image Dimensions: {} {}", .{ img.width, img.height }); + const texture = core.device.createTexture(&.{ + .size = img_size, + .format = .rgba8_unorm, + .usage = .{ + .texture_binding = true, + .copy_dst = true, + .render_attachment = true, + }, + }); + const data_layout = gpu.Texture.DataLayout{ + .bytes_per_row = @as(u32, @intCast(img.width * 4)), + .rows_per_image = @as(u32, @intCast(img.height)), + }; + switch (img.pixels) { + .rgba32 => |pixels| queue.writeTexture(&.{ .texture = texture }, &data_layout, &img_size, pixels), + .rgb24 => |pixels| { + const data = try rgb24ToRgba32(allocator, pixels); + defer data.deinit(allocator); + queue.writeTexture(&.{ .texture = texture }, &data_layout, &img_size, data.rgba32); + }, + else => @panic("unsupported image color format"), + } + + const uniform_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = @sizeOf(UniformBufferObject), + .mapped_at_creation = .false, + }); + + const texture_view = texture.createView(&gpu.TextureView.Descriptor{}); + texture.release(); + + const bind_group_layout = pipeline.getBindGroupLayout(0); + const bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bind_group_layout, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, uniform_buffer, 0, @sizeOf(UniformBufferObject), @sizeOf(UniformBufferObject)), + gpu.BindGroup.Entry.sampler(1, sampler), + gpu.BindGroup.Entry.textureView(2, texture_view), + gpu.BindGroup.Entry.buffer(3, sprites_buffer, 0, @sizeOf(Sprite) * app.sprites.items.len, @sizeOf(Sprite)), + }, + }), + ); + texture_view.release(); + sampler.release(); + bind_group_layout.release(); + + app.title_timer = try core.Timer.start(); + app.timer = try core.Timer.start(); + app.fps_timer = try core.Timer.start(); + app.pipeline = pipeline; + app.uniform_buffer = uniform_buffer; + app.bind_group = bind_group; + app.sprites_buffer = sprites_buffer; + + shader_module.release(); +} + +pub fn deinit(app: *App) void { + defer _ = gpa.deinit(); + defer core.deinit(); + + app.pipeline.release(); + app.sprites.deinit(); + app.sprites_frames.deinit(); + app.uniform_buffer.release(); + app.bind_group.release(); + app.sprites_buffer.release(); +} + +pub fn update(app: *App) !bool { + // Handle input by determining the direction the player wants to go. + var iter = core.pollEvents(); + while (iter.next()) |event| { + switch (event) { + .key_press => |ev| { + switch (ev.key) { + .space => return true, + .left => app.direction[0] += 1, + .right => app.direction[0] -= 1, + .up => app.direction[1] += 1, + .down => app.direction[1] -= 1, + else => {}, + } + }, + .key_release => |ev| { + switch (ev.key) { + .left => app.direction[0] -= 1, + .right => app.direction[0] += 1, + .up => app.direction[1] -= 1, + .down => app.direction[1] += 1, + else => {}, + } + }, + .close => return true, + else => {}, + } + } + + // Calculate the player position, by moving in the direction the player wants to go + // by the speed amount. Multiply by delta_time to ensure that movement is the same speed + // regardless of the frame rate. + const delta_time = app.fps_timer.lap(); + app.player_pos += app.direction * Vec2{ speed, speed } * Vec2{ delta_time, delta_time }; + + // Render the frame + try app.render(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Sprite2D [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} + +fn render(app: *App) !void { + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + // sky blue background color: + .clear_value = .{ .r = 0.52, .g = 0.8, .b = 0.92, .a = 1.0 }, + .load_op = .clear, + .store_op = .store, + }; + + const encoder = core.device.createCommandEncoder(null); + const render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + }); + + const player_sprite = &app.sprites.items[app.player_sprite_index]; + const player_sprite_frame = &app.sprites_frames.items[app.player_sprite_index]; + if (app.direction[0] == -1.0) { + player_sprite.pos = player_sprite_frame.left; + } else if (app.direction[0] == 1.0) { + player_sprite.pos = player_sprite_frame.right; + } else if (app.direction[1] == -1.0) { + player_sprite.pos = player_sprite_frame.down; + } else if (app.direction[1] == 1.0) { + player_sprite.pos = player_sprite_frame.up; + } + player_sprite.world_pos = app.player_pos; + + // One pixel in our scene will equal one window pixel (i.e. be roughly the same size + // irrespective of whether the user has a Retina/HDPI display.) + const proj = zm.orthographicRh( + @as(f32, @floatFromInt(core.size().width)), + @as(f32, @floatFromInt(core.size().height)), + 0.1, + 1000, + ); + const view = zm.lookAtRh( + zm.Vec{ 0, 1000, 0, 1 }, + zm.Vec{ 0, 0, 0, 1 }, + zm.Vec{ 0, 0, 1, 0 }, + ); + const mvp = zm.mul(view, proj); + const ubo = UniformBufferObject{ + .mat = zm.transpose(mvp), + }; + + // Pass the latest uniform values & sprite values to the shader program. + encoder.writeBuffer(app.uniform_buffer, 0, &[_]UniformBufferObject{ubo}); + encoder.writeBuffer(app.sprites_buffer, 0, app.sprites.items); + + // Draw the sprite batch + const total_vertices = @as(u32, @intCast(app.sprites.items.len * 6)); + const pass = encoder.beginRenderPass(&render_pass_info); + pass.setPipeline(app.pipeline); + pass.setBindGroup(0, app.bind_group, &.{}); + pass.draw(total_vertices, 1, 0, 0); + pass.end(); + pass.release(); + + // Submit the frame. + var command = encoder.finish(null); + encoder.release(); + const queue = core.queue; + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); +} + +fn rgb24ToRgba32(allocator: std.mem.Allocator, in: []zigimg.color.Rgb24) !zigimg.color.PixelStorage { + const out = try zigimg.color.PixelStorage.init(allocator, .rgba32, in.len); + var i: usize = 0; + while (i < in.len) : (i += 1) { + out.rgba32[i] = zigimg.color.Rgba32{ .r = in[i].r, .g = in[i].g, .b = in[i].b, .a = 255 }; + } + return out; +} diff --git a/src/core/examples/sysgpu/sprite2d/sprite-shader.wgsl b/src/core/examples/sysgpu/sprite2d/sprite-shader.wgsl new file mode 100644 index 00000000..979c14de --- /dev/null +++ b/src/core/examples/sysgpu/sprite2d/sprite-shader.wgsl @@ -0,0 +1,82 @@ +struct Uniforms { + modelViewProjectionMatrix : mat4x4, +}; +@binding(0) @group(0) var uniforms : Uniforms; + +struct VertexOutput { + @builtin(position) Position : vec4, + @location(0) fragUV : vec2, + @location(1) spriteIndex : f32, +}; + +struct Sprite { + pos: vec2, + size: vec2, + world_pos: vec2, + sheet_size: vec2, +}; +@binding(3) @group(0) var sprites: array; + +@vertex +fn vertex_main( + @builtin(vertex_index) VertexIndex : u32 +) -> VertexOutput { + var sprite = sprites[VertexIndex / 6]; + + // Calculate the vertex position + var positions = array, 6>( + vec2(0.0, 0.0), // bottom-left + vec2(0.0, 1.0), // top-left + vec2(1.0, 0.0), // bottom-right + vec2(1.0, 0.0), // bottom-right + vec2(0.0, 1.0), // top-left + vec2(1.0, 1.0), // top-right + ); + var pos = positions[VertexIndex % 6]; + pos.x *= sprite.size.x; + pos.y *= sprite.size.y; + pos.x += sprite.world_pos.x; + pos.y += sprite.world_pos.y; + + // Calculate the UV coordinate + var uvs = array, 6>( + vec2(0.0, 1.0), // bottom-left + vec2(0.0, 0.0), // top-left + vec2(1.0, 1.0), // bottom-right + vec2(1.0, 1.0), // bottom-right + vec2(0.0, 0.0), // top-left + vec2(1.0, 0.0), // top-right + ); + var uv = uvs[VertexIndex % 6]; + uv.x *= sprite.size.x / sprite.sheet_size.x; + uv.y *= sprite.size.y / sprite.sheet_size.y; + uv.x += sprite.pos.x / sprite.sheet_size.x; + uv.y += sprite.pos.y / sprite.sheet_size.y; + + var output : VertexOutput; + output.Position = vec4(pos.x, 0.0, pos.y, 1.0) * uniforms.modelViewProjectionMatrix; + output.fragUV = uv; + output.spriteIndex = f32(VertexIndex / 6); + return output; +} + +@group(0) @binding(1) var spriteSampler: sampler; +@group(0) @binding(2) var spriteTexture: texture_2d; + +@fragment +fn frag_main( + @location(0) fragUV: vec2, + @location(1) spriteIndex: f32 +) -> @location(0) vec4 { + var color = textureSample(spriteTexture, spriteSampler, fragUV); + if (spriteIndex == 0.0) { + if (color[3] > 0.0) { + color[0] = 0.3; + color[1] = 0.2; + color[2] = 0.5; + color[3] = 1.0; + } + } + + return color; +} \ No newline at end of file diff --git a/src/core/examples/sysgpu/sprite2d/sprites.json b/src/core/examples/sysgpu/sprite2d/sprites.json new file mode 100644 index 00000000..37d5ab13 --- /dev/null +++ b/src/core/examples/sysgpu/sprite2d/sprites.json @@ -0,0 +1,53 @@ +{ + "sheet": { + "width": 352.0, + "height": 32.0 + }, + "sprites": [ + { + "pos": [ 0.0, 0.0 ], + "size": [ 32.0, 32.0 ], + "world_pos": [ 0.0, 0.0 ], + "is_player": true, + "frames": { + "down": [ 0.0, 0.0 ], + "left": [ 32.0, 0.0 ], + "right": [ 64.0, 0.0 ], + "up": [ 96.0, 0.0 ] + } + }, + { + "pos": [ 128.0, 0.0 ], + "size": [ 32.0, 32.0 ], + "world_pos": [ 32.0, 32.0 ], + "frames": { + "down": [ 128.0, 0.0 ], + "left": [ 160.0, 0.0 ], + "right": [ 192.0, 0.0 ], + "up": [ 224.0, 0.0 ] + } + }, + { + "pos": [ 128.0, 0.0 ], + "size": [ 32.0, 32.0 ], + "world_pos": [ 64.0, 64.0 ], + "frames": { + "down": [ 0.0, 0.0 ], + "left": [ 0.0, 0.0 ], + "right": [ 0.0, 0.0 ], + "up": [ 0.0, 0.0 ] + } + }, + { + "pos": [ 256.0, 0.0 ], + "size": [ 32.0, 32.0 ], + "world_pos": [ 96.0, 96.0 ], + "frames": { + "down": [ 0.0, 0.0 ], + "left": [ 0.0, 0.0 ], + "right": [ 0.0, 0.0 ], + "up": [ 0.0, 0.0 ] + } + } + ] +} \ No newline at end of file diff --git a/src/core/examples/sysgpu/textured-cube/cube_mesh.zig b/src/core/examples/sysgpu/textured-cube/cube_mesh.zig new file mode 100644 index 00000000..ae5b2912 --- /dev/null +++ b/src/core/examples/sysgpu/textured-cube/cube_mesh.zig @@ -0,0 +1,49 @@ +pub const Vertex = extern struct { + pos: @Vector(4, f32), + col: @Vector(4, f32), + uv: @Vector(2, f32), +}; + +pub const vertices = [_]Vertex{ + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 1, 0 } }, + + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 0 } }, + + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } }, + + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } }, + + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } }, + + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } }, +}; diff --git a/src/core/examples/sysgpu/textured-cube/main.zig b/src/core/examples/sysgpu/textured-cube/main.zig new file mode 100644 index 00000000..b9119965 --- /dev/null +++ b/src/core/examples/sysgpu/textured-cube/main.zig @@ -0,0 +1,318 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const zm = @import("zmath"); +const zigimg = @import("zigimg"); +const Vertex = @import("cube_mesh.zig").Vertex; +const vertices = @import("cube_mesh.zig").vertices; +const assets = @import("assets"); + +pub const App = @This(); + +pub const mach_core_options = core.ComptimeOptions{ + .use_wgpu = false, + .use_sysgpu = true, +}; + +const UniformBufferObject = struct { + mat: zm.Mat, +}; +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +title_timer: core.Timer, +timer: core.Timer, +pipeline: *gpu.RenderPipeline, +vertex_buffer: *gpu.Buffer, +uniform_buffer: *gpu.Buffer, +bind_group: *gpu.BindGroup, +depth_texture: *gpu.Texture, +depth_texture_view: *gpu.TextureView, + +pub fn init(app: *App) !void { + try core.init(.{}); + const allocator = gpa.allocator(); + + const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); + + const vertex_attributes = [_]gpu.VertexAttribute{ + .{ .format = .float32x4, .offset = @offsetOf(Vertex, "pos"), .shader_location = 0 }, + .{ .format = .float32x2, .offset = @offsetOf(Vertex, "uv"), .shader_location = 1 }, + }; + const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{ + .array_stride = @sizeOf(Vertex), + .step_mode = .vertex, + .attributes = &vertex_attributes, + }); + + const blend = gpu.BlendState{ + .color = .{ + .operation = .add, + .src_factor = .src_alpha, + .dst_factor = .one_minus_src_alpha, + }, + .alpha = .{ + .operation = .add, + .src_factor = .one, + .dst_factor = .zero, + }, + }; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + // Enable depth testing so that the fragment closest to the camera + // is rendered in front. + .depth_stencil = &.{ + .format = .depth24_plus, + .depth_write_enabled = .true, + .depth_compare = .less, + }, + .vertex = gpu.VertexState.init(.{ + .module = shader_module, + .entry_point = "vertex_main", + .buffers = &.{vertex_buffer_layout}, + }), + .primitive = .{ + // Backface culling since the cube is solid piece of geometry. + // Faces pointing away from the camera will be occluded by faces + // pointing toward the camera. + .cull_mode = .back, + }, + }; + const pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + shader_module.release(); + + const vertex_buffer = core.device.createBuffer(&.{ + .usage = .{ .vertex = true }, + .size = @sizeOf(Vertex) * vertices.len, + .mapped_at_creation = .true, + }); + const vertex_mapped = vertex_buffer.getMappedRange(Vertex, 0, vertices.len); + @memcpy(vertex_mapped.?, vertices[0..]); + vertex_buffer.unmap(); + + // Create a sampler with linear filtering for smooth interpolation. + const sampler = core.device.createSampler(&.{ + .mag_filter = .linear, + .min_filter = .linear, + }); + const queue = core.queue; + var img = try zigimg.Image.fromMemory(allocator, assets.gotta_go_fast_png); + defer img.deinit(); + const img_size = gpu.Extent3D{ .width = @as(u32, @intCast(img.width)), .height = @as(u32, @intCast(img.height)) }; + const cube_texture = core.device.createTexture(&.{ + .size = img_size, + .format = .rgba8_unorm, + .usage = .{ + .texture_binding = true, + .copy_dst = true, + .render_attachment = true, + }, + }); + const data_layout = gpu.Texture.DataLayout{ + .bytes_per_row = @as(u32, @intCast(img.width * 4)), + .rows_per_image = @as(u32, @intCast(img.height)), + }; + switch (img.pixels) { + .rgba32 => |pixels| queue.writeTexture(&.{ .texture = cube_texture }, &data_layout, &img_size, pixels), + .rgb24 => |pixels| { + const data = try rgb24ToRgba32(allocator, pixels); + defer data.deinit(allocator); + queue.writeTexture(&.{ .texture = cube_texture }, &data_layout, &img_size, data.rgba32); + }, + else => @panic("unsupported image color format"), + } + + const uniform_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = @sizeOf(UniformBufferObject), + .mapped_at_creation = .false, + }); + + const texture_view = cube_texture.createView(&gpu.TextureView.Descriptor{}); + cube_texture.release(); + + const bind_group_layout = pipeline.getBindGroupLayout(0); + const bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bind_group_layout, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, uniform_buffer, 0, @sizeOf(UniformBufferObject), @sizeOf(UniformBufferObject)), + gpu.BindGroup.Entry.sampler(1, sampler), + gpu.BindGroup.Entry.textureView(2, texture_view), + }, + }), + ); + sampler.release(); + texture_view.release(); + bind_group_layout.release(); + + const depth_texture = core.device.createTexture(&gpu.Texture.Descriptor{ + .size = gpu.Extent3D{ + .width = core.descriptor.width, + .height = core.descriptor.height, + }, + .format = .depth24_plus, + .usage = .{ + .render_attachment = true, + .texture_binding = true, + }, + }); + + const depth_texture_view = depth_texture.createView(&gpu.TextureView.Descriptor{ + .format = .depth24_plus, + .dimension = .dimension_2d, + .array_layer_count = 1, + .mip_level_count = 1, + }); + + app.timer = try core.Timer.start(); + app.title_timer = try core.Timer.start(); + app.pipeline = pipeline; + app.vertex_buffer = vertex_buffer; + app.uniform_buffer = uniform_buffer; + app.bind_group = bind_group; + app.depth_texture = depth_texture; + app.depth_texture_view = depth_texture_view; +} + +pub fn deinit(app: *App) void { + defer _ = gpa.deinit(); + defer core.deinit(); + + app.pipeline.release(); + app.vertex_buffer.release(); + app.uniform_buffer.release(); + app.bind_group.release(); + app.depth_texture.release(); + app.depth_texture_view.release(); +} + +pub fn update(app: *App) !bool { + var iter = core.pollEvents(); + while (iter.next()) |event| { + switch (event) { + .key_press => |ev| { + switch (ev.key) { + .space => return true, + .one => core.setVSync(.none), + .two => core.setVSync(.double), + .three => core.setVSync(.triple), + else => {}, + } + std.debug.print("vsync mode changed to {s}\n", .{@tagName(core.vsync())}); + }, + .framebuffer_resize => |ev| { + // If window is resized, recreate depth buffer otherwise we cannot use it. + app.depth_texture.release(); + + app.depth_texture = core.device.createTexture(&gpu.Texture.Descriptor{ + .size = gpu.Extent3D{ + .width = ev.width, + .height = ev.height, + }, + .format = .depth24_plus, + .usage = .{ + .render_attachment = true, + .texture_binding = true, + }, + }); + + app.depth_texture_view.release(); + app.depth_texture_view = app.depth_texture.createView(&gpu.TextureView.Descriptor{ + .format = .depth24_plus, + .dimension = .dimension_2d, + .array_layer_count = 1, + .mip_level_count = 1, + }); + }, + .close => return true, + else => {}, + } + } + + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = .{ .r = 0.5, .g = 0.5, .b = 0.5, .a = 0.0 }, + .load_op = .clear, + .store_op = .store, + }; + + const encoder = core.device.createCommandEncoder(null); + const render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + .depth_stencil_attachment = &.{ + .view = app.depth_texture_view, + .depth_clear_value = 1.0, + .depth_load_op = .clear, + .depth_store_op = .store, + }, + }); + + { + const time = app.timer.read(); + const model = zm.mul(zm.rotationX(time * (std.math.pi / 2.0)), zm.rotationZ(time * (std.math.pi / 2.0))); + const view = zm.lookAtRh( + zm.Vec{ 0, 4, 2, 1 }, + zm.Vec{ 0, 0, 0, 1 }, + zm.Vec{ 0, 0, 1, 0 }, + ); + const proj = zm.perspectiveFovRh( + (std.math.pi / 4.0), + @as(f32, @floatFromInt(core.descriptor.width)) / @as(f32, @floatFromInt(core.descriptor.height)), + 0.1, + 10, + ); + const mvp = zm.mul(zm.mul(model, view), proj); + const ubo = UniformBufferObject{ + .mat = zm.transpose(mvp), + }; + encoder.writeBuffer(app.uniform_buffer, 0, &[_]UniformBufferObject{ubo}); + } + + const pass = encoder.beginRenderPass(&render_pass_info); + pass.setPipeline(app.pipeline); + pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len); + pass.setBindGroup(0, app.bind_group, &.{}); + pass.draw(vertices.len, 1, 0, 0); + pass.end(); + pass.release(); + + var command = encoder.finish(null); + encoder.release(); + + const queue = core.queue; + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Textured cube [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + return false; +} + +fn rgb24ToRgba32(allocator: std.mem.Allocator, in: []zigimg.color.Rgb24) !zigimg.color.PixelStorage { + const out = try zigimg.color.PixelStorage.init(allocator, .rgba32, in.len); + var i: usize = 0; + while (i < in.len) : (i += 1) { + out.rgba32[i] = zigimg.color.Rgba32{ .r = in[i].r, .g = in[i].g, .b = in[i].b, .a = 255 }; + } + return out; +} diff --git a/src/core/examples/sysgpu/textured-cube/shader.wgsl b/src/core/examples/sysgpu/textured-cube/shader.wgsl new file mode 100644 index 00000000..7e8b5422 --- /dev/null +++ b/src/core/examples/sysgpu/textured-cube/shader.wgsl @@ -0,0 +1,29 @@ +struct Uniforms { + modelViewProjectionMatrix : mat4x4, +}; +@binding(0) @group(0) var uniforms : Uniforms; + +struct VertexOutput { + @builtin(position) Position : vec4, + @location(0) fragUV : vec2, + @location(1) fragPosition: vec4, +}; + +@vertex +fn vertex_main(@location(0) position : vec4, + @location(1) uv : vec2) -> VertexOutput { + var output : VertexOutput; + output.Position = position * uniforms.modelViewProjectionMatrix; + output.fragUV = uv; + output.fragPosition = 0.5 * (position + vec4(1.0, 1.0, 1.0, 1.0)); + return output; +} + +@group(0) @binding(1) var mySampler: sampler; +@group(0) @binding(2) var myTexture: texture_2d; + +@fragment +fn frag_main(@location(0) fragUV: vec2, + @location(1) fragPosition: vec4) -> @location(0) vec4 { + return textureSample(myTexture, mySampler, fragUV); +} \ No newline at end of file diff --git a/src/core/examples/sysgpu/textured-quad/main.zig b/src/core/examples/sysgpu/textured-quad/main.zig new file mode 100644 index 00000000..a27f5da7 --- /dev/null +++ b/src/core/examples/sysgpu/textured-quad/main.zig @@ -0,0 +1,209 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const zigimg = @import("zigimg"); +const assets = @import("assets"); + +pub const App = @This(); + +const Vertex = extern struct { + pos: @Vector(2, f32), + uv: @Vector(2, f32), +}; + +const vertices = [_]Vertex{ + .{ .pos = .{ -0.5, -0.5 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 0.5, -0.5 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 0.5, 0.5 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -0.5, 0.5 }, .uv = .{ 1, 0 } }, +}; +const index_data = [_]u32{ 0, 1, 2, 2, 3, 0 }; + +pub const mach_core_options = core.ComptimeOptions{ + .use_wgpu = false, + .use_sysgpu = true, +}; + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +title_timer: core.Timer, +timer: core.Timer, +pipeline: *gpu.RenderPipeline, +vertex_buffer: *gpu.Buffer, +index_buffer: *gpu.Buffer, +bind_group: *gpu.BindGroup, + +pub fn init(app: *App) !void { + try core.init(.{}); + const allocator = gpa.allocator(); + + const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); + + const vertex_attributes = [_]gpu.VertexAttribute{ + .{ .format = .float32x4, .offset = @offsetOf(Vertex, "pos"), .shader_location = 0 }, + .{ .format = .float32x2, .offset = @offsetOf(Vertex, "uv"), .shader_location = 1 }, + }; + const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{ + .array_stride = @sizeOf(Vertex), + .step_mode = .vertex, + .attributes = &vertex_attributes, + }); + + const blend = gpu.BlendState{}; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + .vertex = gpu.VertexState.init(.{ + .module = shader_module, + .entry_point = "vertex_main", + .buffers = &.{vertex_buffer_layout}, + }), + .primitive = .{ .cull_mode = .back }, + }; + const pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + shader_module.release(); + + const vertex_buffer = core.device.createBuffer(&.{ + .usage = .{ .vertex = true }, + .size = @sizeOf(Vertex) * vertices.len, + .mapped_at_creation = .true, + }); + const vertex_mapped = vertex_buffer.getMappedRange(Vertex, 0, vertices.len); + @memcpy(vertex_mapped.?, vertices[0..]); + vertex_buffer.unmap(); + + const index_buffer = core.device.createBuffer(&.{ + .usage = .{ .index = true }, + .size = @sizeOf(u32) * index_data.len, + .mapped_at_creation = .true, + }); + const index_mapped = index_buffer.getMappedRange(u32, 0, index_data.len); + @memcpy(index_mapped.?, index_data[0..]); + index_buffer.unmap(); + + const sampler = core.device.createSampler(&.{ .mag_filter = .linear, .min_filter = .linear }); + const queue = core.queue; + var img = try zigimg.Image.fromMemory(allocator, assets.gotta_go_fast_png); + defer img.deinit(); + const img_size = gpu.Extent3D{ + .width = @as(u32, @intCast(img.width)), + .height = @as(u32, @intCast(img.height)), + }; + const texture = core.device.createTexture(&.{ + .size = img_size, + .format = .rgba8_unorm, + .usage = .{ + .texture_binding = true, + .copy_dst = true, + .render_attachment = true, + }, + }); + const data_layout = gpu.Texture.DataLayout{ + .bytes_per_row = @as(u32, @intCast(img.width * 4)), + .rows_per_image = @as(u32, @intCast(img.height)), + }; + switch (img.pixels) { + .rgba32 => |pixels| queue.writeTexture(&.{ .texture = texture }, &data_layout, &img_size, pixels), + .rgb24 => |pixels| { + const data = try rgb24ToRgba32(allocator, pixels); + defer data.deinit(allocator); + queue.writeTexture(&.{ .texture = texture }, &data_layout, &img_size, data.rgba32); + }, + else => @panic("unsupported image color format"), + } + + const texture_view = texture.createView(&gpu.TextureView.Descriptor{}); + texture.release(); + + const bind_group_layout = pipeline.getBindGroupLayout(0); + const bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bind_group_layout, + .entries = &.{ + gpu.BindGroup.Entry.sampler(0, sampler), + gpu.BindGroup.Entry.textureView(1, texture_view), + }, + }), + ); + sampler.release(); + texture_view.release(); + bind_group_layout.release(); + + app.timer = try core.Timer.start(); + app.title_timer = try core.Timer.start(); + app.pipeline = pipeline; + app.vertex_buffer = vertex_buffer; + app.index_buffer = index_buffer; + app.bind_group = bind_group; +} + +pub fn deinit(app: *App) void { + app.pipeline.release(); + app.vertex_buffer.release(); + app.index_buffer.release(); + app.bind_group.release(); + core.deinit(); + _ = gpa.deinit(); +} + +pub fn update(app: *App) !bool { + var iter = core.pollEvents(); + while (iter.next()) |event| if (event == .close) return true; + + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = .{ .r = 0, .g = 0, .b = 0, .a = 0.0 }, + .load_op = .clear, + .store_op = .store, + }; + + const encoder = core.device.createCommandEncoder(null); + const render_pass_info = gpu.RenderPassDescriptor.init(.{ .color_attachments = &.{color_attachment} }); + + const pass = encoder.beginRenderPass(&render_pass_info); + pass.setPipeline(app.pipeline); + pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len); + pass.setIndexBuffer(app.index_buffer, .uint32, 0, @sizeOf(u32) * index_data.len); + pass.setBindGroup(0, app.bind_group, &.{}); + pass.drawIndexed(index_data.len, 1, 0, 0, 0); + pass.end(); + pass.release(); + + var command = encoder.finish(null); + encoder.release(); + + const queue = core.queue; + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Textured Quad [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + return false; +} + +fn rgb24ToRgba32(allocator: std.mem.Allocator, in: []zigimg.color.Rgb24) !zigimg.color.PixelStorage { + const out = try zigimg.color.PixelStorage.init(allocator, .rgba32, in.len); + var i: usize = 0; + while (i < in.len) : (i += 1) { + out.rgba32[i] = zigimg.color.Rgba32{ .r = in[i].r, .g = in[i].g, .b = in[i].b, .a = 255 }; + } + return out; +} diff --git a/src/core/examples/sysgpu/textured-quad/shader.wgsl b/src/core/examples/sysgpu/textured-quad/shader.wgsl new file mode 100644 index 00000000..3328340b --- /dev/null +++ b/src/core/examples/sysgpu/textured-quad/shader.wgsl @@ -0,0 +1,21 @@ +struct VertexOutput { + @builtin(position) Position : vec4, + @location(0) fragUV : vec2, +}; + +@vertex +fn vertex_main(@location(0) position : vec2, + @location(1) uv : vec2) -> VertexOutput { + var output : VertexOutput; + output.Position = vec4(position, 0, 1); + output.fragUV = uv; + return output; +} + +@group(0) @binding(0) var mySampler: sampler; +@group(0) @binding(1) var myTexture: texture_2d; + +@fragment +fn frag_main(@location(0) fragUV: vec2) -> @location(0) vec4 { + return textureSample(myTexture, mySampler, fragUV); +} \ No newline at end of file diff --git a/src/core/examples/sysgpu/triangle-msaa/main.zig b/src/core/examples/sysgpu/triangle-msaa/main.zig new file mode 100644 index 00000000..eac21780 --- /dev/null +++ b/src/core/examples/sysgpu/triangle-msaa/main.zig @@ -0,0 +1,133 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; + +pub const App = @This(); + +pub const mach_core_options = core.ComptimeOptions{ + .use_wgpu = false, + .use_sysgpu = true, +}; + +title_timer: core.Timer, +pipeline: *gpu.RenderPipeline, +texture: *gpu.Texture, +texture_view: *gpu.TextureView, + +const sample_count = 4; + +pub fn init(app: *App) !void { + try core.init(.{}); + app.title_timer = try core.Timer.start(); + + const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); + defer shader_module.release(); + + // Fragment state + const blend = gpu.BlendState{}; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + .vertex = gpu.VertexState{ + .module = shader_module, + .entry_point = "vertex_main", + }, + .multisample = gpu.MultisampleState{ + .count = sample_count, + }, + }; + + app.pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + + app.texture = core.device.createTexture(&gpu.Texture.Descriptor{ + .size = gpu.Extent3D{ + .width = core.descriptor.width, + .height = core.descriptor.height, + }, + .sample_count = sample_count, + .format = core.descriptor.format, + .usage = .{ .render_attachment = true }, + }); + app.texture_view = app.texture.createView(null); +} + +pub fn deinit(app: *App) void { + defer core.deinit(); + + app.pipeline.release(); + app.texture.release(); + app.texture_view.release(); +} + +pub fn update(app: *App) !bool { + var iter = core.pollEvents(); + while (iter.next()) |event| { + switch (event) { + .framebuffer_resize => |size| { + app.texture.release(); + app.texture = core.device.createTexture(&gpu.Texture.Descriptor{ + .size = gpu.Extent3D{ + .width = size.width, + .height = size.height, + }, + .sample_count = sample_count, + .format = core.descriptor.format, + .usage = .{ .render_attachment = true }, + }); + + app.texture_view.release(); + app.texture_view = app.texture.createView(null); + }, + .close => return true, + else => {}, + } + } + + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const color_attachment = gpu.RenderPassColorAttachment{ + .view = app.texture_view, + .resolve_target = back_buffer_view, + .clear_value = std.mem.zeroes(gpu.Color), + .load_op = .clear, + .store_op = .discard, + }; + + const encoder = core.device.createCommandEncoder(null); + const render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + }); + const pass = encoder.beginRenderPass(&render_pass_info); + pass.setPipeline(app.pipeline); + pass.draw(3, 1, 0, 0); + pass.end(); + pass.release(); + + var command = encoder.finish(null); + encoder.release(); + + const queue = core.queue; + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Triangle MSAA [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} diff --git a/src/core/examples/sysgpu/triangle-msaa/shader.wgsl b/src/core/examples/sysgpu/triangle-msaa/shader.wgsl new file mode 100644 index 00000000..429d87e0 --- /dev/null +++ b/src/core/examples/sysgpu/triangle-msaa/shader.wgsl @@ -0,0 +1,14 @@ +@vertex fn vertex_main( + @builtin(vertex_index) VertexIndex : u32 +) -> @builtin(position) vec4 { + var pos = array, 3>( + vec2( 0.0, 0.5), + vec2(-0.5, -0.5), + vec2( 0.5, -0.5) + ); + return vec4(pos[VertexIndex], 0.0, 1.0); +} + +@fragment fn frag_main() -> @location(0) vec4 { + return vec4(1.0, 0.0, 0.0, 1.0); +} diff --git a/src/core/examples/sysgpu/triangle/main.zig b/src/core/examples/sysgpu/triangle/main.zig new file mode 100644 index 00000000..328e7688 --- /dev/null +++ b/src/core/examples/sysgpu/triangle/main.zig @@ -0,0 +1,96 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; + +pub const App = @This(); + +pub const mach_core_options = core.ComptimeOptions{ + .use_wgpu = false, + .use_sysgpu = true, +}; + +title_timer: core.Timer, +pipeline: *gpu.RenderPipeline, + +pub fn init(app: *App) !void { + try core.init(.{}); + + const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); + defer shader_module.release(); + + // Fragment state + const blend = gpu.BlendState{}; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + .vertex = gpu.VertexState{ + .module = shader_module, + .entry_point = "vertex_main", + }, + }; + const pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + + app.* = .{ .title_timer = try core.Timer.start(), .pipeline = pipeline }; +} + +pub fn deinit(app: *App) void { + defer core.deinit(); + app.pipeline.release(); +} + +pub fn update(app: *App) !bool { + var iter = core.pollEvents(); + while (iter.next()) |event| { + switch (event) { + .close => return true, + else => {}, + } + } + + const queue = core.queue; + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = std.mem.zeroes(gpu.Color), + .load_op = .clear, + .store_op = .store, + }; + + const encoder = core.device.createCommandEncoder(null); + const render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + }); + const pass = encoder.beginRenderPass(&render_pass_info); + pass.setPipeline(app.pipeline); + pass.draw(3, 1, 0, 0); + pass.end(); + pass.release(); + + var command = encoder.finish(null); + encoder.release(); + + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Triangle [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} diff --git a/src/core/examples/sysgpu/triangle/shader.wgsl b/src/core/examples/sysgpu/triangle/shader.wgsl new file mode 100644 index 00000000..429d87e0 --- /dev/null +++ b/src/core/examples/sysgpu/triangle/shader.wgsl @@ -0,0 +1,14 @@ +@vertex fn vertex_main( + @builtin(vertex_index) VertexIndex : u32 +) -> @builtin(position) vec4 { + var pos = array, 3>( + vec2( 0.0, 0.5), + vec2(-0.5, -0.5), + vec2( 0.5, -0.5) + ); + return vec4(pos[VertexIndex], 0.0, 1.0); +} + +@fragment fn frag_main() -> @location(0) vec4 { + return vec4(1.0, 0.0, 0.0, 1.0); +} diff --git a/src/core/examples/sysgpu/two-cubes/cube_mesh.zig b/src/core/examples/sysgpu/two-cubes/cube_mesh.zig new file mode 100644 index 00000000..f26c75ac --- /dev/null +++ b/src/core/examples/sysgpu/two-cubes/cube_mesh.zig @@ -0,0 +1,49 @@ +pub const Vertex = extern struct { + pos: @Vector(4, f32), + col: @Vector(4, f32), + uv: @Vector(2, f32), +}; + +pub const vertices = [_]Vertex{ + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, +}; diff --git a/src/core/examples/sysgpu/two-cubes/main.zig b/src/core/examples/sysgpu/two-cubes/main.zig new file mode 100644 index 00000000..d3ad509e --- /dev/null +++ b/src/core/examples/sysgpu/two-cubes/main.zig @@ -0,0 +1,228 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const zm = @import("zmath"); +const Vertex = @import("cube_mesh.zig").Vertex; +const vertices = @import("cube_mesh.zig").vertices; + +const UniformBufferObject = struct { + mat: zm.Mat, +}; + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +title_timer: core.Timer, +timer: core.Timer, +pipeline: *gpu.RenderPipeline, +vertex_buffer: *gpu.Buffer, +uniform_buffer: *gpu.Buffer, +bind_group1: *gpu.BindGroup, +bind_group2: *gpu.BindGroup, + +pub const App = @This(); + +pub const mach_core_options = core.ComptimeOptions{ + .use_wgpu = false, + .use_sysgpu = true, +}; + +pub fn init(app: *App) !void { + try core.init(.{}); + app.title_timer = try core.Timer.start(); + app.timer = try core.Timer.start(); + + const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); + + const vertex_attributes = [_]gpu.VertexAttribute{ + .{ .format = .float32x4, .offset = @offsetOf(Vertex, "pos"), .shader_location = 0 }, + .{ .format = .float32x2, .offset = @offsetOf(Vertex, "uv"), .shader_location = 1 }, + }; + const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{ + .array_stride = @sizeOf(Vertex), + .step_mode = .vertex, + .attributes = &vertex_attributes, + }); + + const blend = gpu.BlendState{}; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + + const bgle = gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, true, 0); + const bgl = core.device.createBindGroupLayout( + &gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &.{bgle}, + }), + ); + + const bind_group_layouts = [_]*gpu.BindGroupLayout{bgl}; + const pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &bind_group_layouts, + })); + + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + .layout = pipeline_layout, + .vertex = gpu.VertexState.init(.{ + .module = shader_module, + .entry_point = "vertex_main", + .buffers = &.{vertex_buffer_layout}, + }), + .primitive = .{ + .cull_mode = .back, + }, + }; + + const vertex_buffer = core.device.createBuffer(&.{ + .usage = .{ .vertex = true }, + .size = @sizeOf(Vertex) * vertices.len, + .mapped_at_creation = .true, + }); + const vertex_mapped = vertex_buffer.getMappedRange(Vertex, 0, vertices.len); + @memcpy(vertex_mapped.?, vertices[0..]); + vertex_buffer.unmap(); + + // uniformBindGroup offset must be 256-byte aligned + const uniform_offset = 256; + const uniform_buffer = core.device.createBuffer(&.{ + .usage = .{ .uniform = true, .copy_dst = true }, + .size = @sizeOf(UniformBufferObject) + uniform_offset, + .mapped_at_creation = .false, + }); + + const bind_group1 = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bgl, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, uniform_buffer, 0, @sizeOf(UniformBufferObject), @sizeOf(UniformBufferObject)), + }, + }), + ); + + const bind_group2 = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bgl, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, uniform_buffer, uniform_offset, @sizeOf(UniformBufferObject), @sizeOf(UniformBufferObject)), + }, + }), + ); + + app.pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + app.vertex_buffer = vertex_buffer; + app.uniform_buffer = uniform_buffer; + app.bind_group1 = bind_group1; + app.bind_group2 = bind_group2; + + shader_module.release(); + pipeline_layout.release(); + bgl.release(); +} + +pub fn deinit(app: *App) void { + defer _ = gpa.deinit(); + defer core.deinit(); + + app.pipeline.release(); + app.vertex_buffer.release(); + app.uniform_buffer.release(); + app.bind_group1.release(); + app.bind_group2.release(); +} + +pub fn update(app: *App) !bool { + var iter = core.pollEvents(); + while (iter.next()) |event| { + switch (event) { + .key_press => |ev| { + if (ev.key == .space) return true; + }, + .close => return true, + else => {}, + } + } + + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = std.mem.zeroes(gpu.Color), + .load_op = .clear, + .store_op = .store, + }; + + const encoder = core.device.createCommandEncoder(null); + const render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + }); + + { + const time = app.timer.read(); + const rotation1 = zm.mul(zm.rotationX(time * (std.math.pi / 2.0)), zm.rotationZ(time * (std.math.pi / 2.0))); + const rotation2 = zm.mul(zm.rotationZ(time * (std.math.pi / 2.0)), zm.rotationX(time * (std.math.pi / 2.0))); + const model1 = zm.mul(rotation1, zm.translation(-2, 0, 0)); + const model2 = zm.mul(rotation2, zm.translation(2, 0, 0)); + const view = zm.lookAtRh( + zm.Vec{ 0, -4, 2, 1 }, + zm.Vec{ 0, 0, 0, 1 }, + zm.Vec{ 0, 0, 1, 0 }, + ); + const proj = zm.perspectiveFovRh( + (2.0 * std.math.pi / 5.0), + @as(f32, @floatFromInt(core.descriptor.width)) / @as(f32, @floatFromInt(core.descriptor.height)), + 1, + 100, + ); + const mvp1 = zm.mul(zm.mul(model1, view), proj); + const mvp2 = zm.mul(zm.mul(model2, view), proj); + const ubo1 = UniformBufferObject{ + .mat = zm.transpose(mvp1), + }; + const ubo2 = UniformBufferObject{ + .mat = zm.transpose(mvp2), + }; + + core.queue.writeBuffer(app.uniform_buffer, 0, &[_]UniformBufferObject{ubo1}); + + // bind_group2 offset + core.queue.writeBuffer(app.uniform_buffer, 256, &[_]UniformBufferObject{ubo2}); + } + + const pass = encoder.beginRenderPass(&render_pass_info); + pass.setPipeline(app.pipeline); + pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len); + + pass.setBindGroup(0, app.bind_group1, &.{0}); + pass.draw(vertices.len, 1, 0, 0); + pass.setBindGroup(0, app.bind_group2, &.{0}); + pass.draw(vertices.len, 1, 0, 0); + + pass.end(); + pass.release(); + + var command = encoder.finish(null); + encoder.release(); + + const queue = core.queue; + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Two Cubes [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} diff --git a/src/core/examples/sysgpu/two-cubes/shader.wgsl b/src/core/examples/sysgpu/two-cubes/shader.wgsl new file mode 100644 index 00000000..05384f41 --- /dev/null +++ b/src/core/examples/sysgpu/two-cubes/shader.wgsl @@ -0,0 +1,24 @@ +@group(0) @binding(0) var ubo : mat4x4; +struct VertexOut { + @builtin(position) position_clip : vec4, + @location(0) fragUV : vec2, + @location(1) fragPosition: vec4, +} + +@vertex fn vertex_main( + @location(0) position : vec4, + @location(1) uv: vec2 +) -> VertexOut { + var output : VertexOut; + output.position_clip = position * ubo; + output.fragUV = uv; + output.fragPosition = 0.5 * (position + vec4(1.0, 1.0, 1.0, 1.0)); + return output; +} + +@fragment fn frag_main( + @location(0) fragUV: vec2, + @location(1) fragPosition: vec4 +) -> @location(0) vec4 { + return fragPosition; +} diff --git a/src/core/examples/textured-cube/cube_mesh.zig b/src/core/examples/textured-cube/cube_mesh.zig new file mode 100644 index 00000000..ae5b2912 --- /dev/null +++ b/src/core/examples/textured-cube/cube_mesh.zig @@ -0,0 +1,49 @@ +pub const Vertex = extern struct { + pos: @Vector(4, f32), + col: @Vector(4, f32), + uv: @Vector(2, f32), +}; + +pub const vertices = [_]Vertex{ + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 1, 0 } }, + + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 0 } }, + + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } }, + + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } }, + + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } }, + + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } }, +}; diff --git a/src/core/examples/textured-cube/main.zig b/src/core/examples/textured-cube/main.zig new file mode 100644 index 00000000..7b0f2347 --- /dev/null +++ b/src/core/examples/textured-cube/main.zig @@ -0,0 +1,313 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const zm = @import("zmath"); +const zigimg = @import("zigimg"); +const Vertex = @import("cube_mesh.zig").Vertex; +const vertices = @import("cube_mesh.zig").vertices; +const assets = @import("assets"); + +pub const App = @This(); + +const UniformBufferObject = struct { + mat: zm.Mat, +}; +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +title_timer: core.Timer, +timer: core.Timer, +pipeline: *gpu.RenderPipeline, +vertex_buffer: *gpu.Buffer, +uniform_buffer: *gpu.Buffer, +bind_group: *gpu.BindGroup, +depth_texture: *gpu.Texture, +depth_texture_view: *gpu.TextureView, + +pub fn init(app: *App) !void { + try core.init(.{}); + const allocator = gpa.allocator(); + + const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); + + const vertex_attributes = [_]gpu.VertexAttribute{ + .{ .format = .float32x4, .offset = @offsetOf(Vertex, "pos"), .shader_location = 0 }, + .{ .format = .float32x2, .offset = @offsetOf(Vertex, "uv"), .shader_location = 1 }, + }; + const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{ + .array_stride = @sizeOf(Vertex), + .step_mode = .vertex, + .attributes = &vertex_attributes, + }); + + const blend = gpu.BlendState{ + .color = .{ + .operation = .add, + .src_factor = .src_alpha, + .dst_factor = .one_minus_src_alpha, + }, + .alpha = .{ + .operation = .add, + .src_factor = .one, + .dst_factor = .zero, + }, + }; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + // Enable depth testing so that the fragment closest to the camera + // is rendered in front. + .depth_stencil = &.{ + .format = .depth24_plus, + .depth_write_enabled = .true, + .depth_compare = .less, + }, + .vertex = gpu.VertexState.init(.{ + .module = shader_module, + .entry_point = "vertex_main", + .buffers = &.{vertex_buffer_layout}, + }), + .primitive = .{ + // Backface culling since the cube is solid piece of geometry. + // Faces pointing away from the camera will be occluded by faces + // pointing toward the camera. + .cull_mode = .back, + }, + }; + const pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + shader_module.release(); + + const vertex_buffer = core.device.createBuffer(&.{ + .usage = .{ .vertex = true }, + .size = @sizeOf(Vertex) * vertices.len, + .mapped_at_creation = .true, + }); + const vertex_mapped = vertex_buffer.getMappedRange(Vertex, 0, vertices.len); + @memcpy(vertex_mapped.?, vertices[0..]); + vertex_buffer.unmap(); + + // Create a sampler with linear filtering for smooth interpolation. + const sampler = core.device.createSampler(&.{ + .mag_filter = .linear, + .min_filter = .linear, + }); + const queue = core.queue; + var img = try zigimg.Image.fromMemory(allocator, assets.gotta_go_fast_png); + defer img.deinit(); + const img_size = gpu.Extent3D{ .width = @as(u32, @intCast(img.width)), .height = @as(u32, @intCast(img.height)) }; + const cube_texture = core.device.createTexture(&.{ + .size = img_size, + .format = .rgba8_unorm, + .usage = .{ + .texture_binding = true, + .copy_dst = true, + .render_attachment = true, + }, + }); + const data_layout = gpu.Texture.DataLayout{ + .bytes_per_row = @as(u32, @intCast(img.width * 4)), + .rows_per_image = @as(u32, @intCast(img.height)), + }; + switch (img.pixels) { + .rgba32 => |pixels| queue.writeTexture(&.{ .texture = cube_texture }, &data_layout, &img_size, pixels), + .rgb24 => |pixels| { + const data = try rgb24ToRgba32(allocator, pixels); + defer data.deinit(allocator); + queue.writeTexture(&.{ .texture = cube_texture }, &data_layout, &img_size, data.rgba32); + }, + else => @panic("unsupported image color format"), + } + + const uniform_buffer = core.device.createBuffer(&.{ + .usage = .{ .copy_dst = true, .uniform = true }, + .size = @sizeOf(UniformBufferObject), + .mapped_at_creation = .false, + }); + + const texture_view = cube_texture.createView(&gpu.TextureView.Descriptor{}); + cube_texture.release(); + + const bind_group_layout = pipeline.getBindGroupLayout(0); + const bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bind_group_layout, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, uniform_buffer, 0, @sizeOf(UniformBufferObject)), + gpu.BindGroup.Entry.sampler(1, sampler), + gpu.BindGroup.Entry.textureView(2, texture_view), + }, + }), + ); + sampler.release(); + texture_view.release(); + bind_group_layout.release(); + + const depth_texture = core.device.createTexture(&gpu.Texture.Descriptor{ + .size = gpu.Extent3D{ + .width = core.descriptor.width, + .height = core.descriptor.height, + }, + .format = .depth24_plus, + .usage = .{ + .render_attachment = true, + .texture_binding = true, + }, + }); + + const depth_texture_view = depth_texture.createView(&gpu.TextureView.Descriptor{ + .format = .depth24_plus, + .dimension = .dimension_2d, + .array_layer_count = 1, + .mip_level_count = 1, + }); + + app.timer = try core.Timer.start(); + app.title_timer = try core.Timer.start(); + app.pipeline = pipeline; + app.vertex_buffer = vertex_buffer; + app.uniform_buffer = uniform_buffer; + app.bind_group = bind_group; + app.depth_texture = depth_texture; + app.depth_texture_view = depth_texture_view; +} + +pub fn deinit(app: *App) void { + defer _ = gpa.deinit(); + defer core.deinit(); + + app.pipeline.release(); + app.vertex_buffer.release(); + app.uniform_buffer.release(); + app.bind_group.release(); + app.depth_texture.release(); + app.depth_texture_view.release(); +} + +pub fn update(app: *App) !bool { + var iter = core.pollEvents(); + while (iter.next()) |event| { + switch (event) { + .key_press => |ev| { + switch (ev.key) { + .space => return true, + .one => core.setVSync(.none), + .two => core.setVSync(.double), + .three => core.setVSync(.triple), + else => {}, + } + std.debug.print("vsync mode changed to {s}\n", .{@tagName(core.vsync())}); + }, + .framebuffer_resize => |ev| { + // If window is resized, recreate depth buffer otherwise we cannot use it. + app.depth_texture.release(); + + app.depth_texture = core.device.createTexture(&gpu.Texture.Descriptor{ + .size = gpu.Extent3D{ + .width = ev.width, + .height = ev.height, + }, + .format = .depth24_plus, + .usage = .{ + .render_attachment = true, + .texture_binding = true, + }, + }); + + app.depth_texture_view.release(); + app.depth_texture_view = app.depth_texture.createView(&gpu.TextureView.Descriptor{ + .format = .depth24_plus, + .dimension = .dimension_2d, + .array_layer_count = 1, + .mip_level_count = 1, + }); + }, + .close => return true, + else => {}, + } + } + + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = .{ .r = 0.5, .g = 0.5, .b = 0.5, .a = 0.0 }, + .load_op = .clear, + .store_op = .store, + }; + + const encoder = core.device.createCommandEncoder(null); + const render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + .depth_stencil_attachment = &.{ + .view = app.depth_texture_view, + .depth_clear_value = 1.0, + .depth_load_op = .clear, + .depth_store_op = .store, + }, + }); + + { + const time = app.timer.read(); + const model = zm.mul(zm.rotationX(time * (std.math.pi / 2.0)), zm.rotationZ(time * (std.math.pi / 2.0))); + const view = zm.lookAtRh( + zm.Vec{ 0, 4, 2, 1 }, + zm.Vec{ 0, 0, 0, 1 }, + zm.Vec{ 0, 0, 1, 0 }, + ); + const proj = zm.perspectiveFovRh( + (std.math.pi / 4.0), + @as(f32, @floatFromInt(core.descriptor.width)) / @as(f32, @floatFromInt(core.descriptor.height)), + 0.1, + 10, + ); + const mvp = zm.mul(zm.mul(model, view), proj); + const ubo = UniformBufferObject{ + .mat = zm.transpose(mvp), + }; + encoder.writeBuffer(app.uniform_buffer, 0, &[_]UniformBufferObject{ubo}); + } + + const pass = encoder.beginRenderPass(&render_pass_info); + pass.setPipeline(app.pipeline); + pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len); + pass.setBindGroup(0, app.bind_group, &.{}); + pass.draw(vertices.len, 1, 0, 0); + pass.end(); + pass.release(); + + var command = encoder.finish(null); + encoder.release(); + + const queue = core.queue; + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Textured cube [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + return false; +} + +fn rgb24ToRgba32(allocator: std.mem.Allocator, in: []zigimg.color.Rgb24) !zigimg.color.PixelStorage { + const out = try zigimg.color.PixelStorage.init(allocator, .rgba32, in.len); + var i: usize = 0; + while (i < in.len) : (i += 1) { + out.rgba32[i] = zigimg.color.Rgba32{ .r = in[i].r, .g = in[i].g, .b = in[i].b, .a = 255 }; + } + return out; +} diff --git a/src/core/examples/textured-cube/shader.wgsl b/src/core/examples/textured-cube/shader.wgsl new file mode 100644 index 00000000..7e8b5422 --- /dev/null +++ b/src/core/examples/textured-cube/shader.wgsl @@ -0,0 +1,29 @@ +struct Uniforms { + modelViewProjectionMatrix : mat4x4, +}; +@binding(0) @group(0) var uniforms : Uniforms; + +struct VertexOutput { + @builtin(position) Position : vec4, + @location(0) fragUV : vec2, + @location(1) fragPosition: vec4, +}; + +@vertex +fn vertex_main(@location(0) position : vec4, + @location(1) uv : vec2) -> VertexOutput { + var output : VertexOutput; + output.Position = position * uniforms.modelViewProjectionMatrix; + output.fragUV = uv; + output.fragPosition = 0.5 * (position + vec4(1.0, 1.0, 1.0, 1.0)); + return output; +} + +@group(0) @binding(1) var mySampler: sampler; +@group(0) @binding(2) var myTexture: texture_2d; + +@fragment +fn frag_main(@location(0) fragUV: vec2, + @location(1) fragPosition: vec4) -> @location(0) vec4 { + return textureSample(myTexture, mySampler, fragUV); +} \ No newline at end of file diff --git a/src/core/examples/textured-quad/main.zig b/src/core/examples/textured-quad/main.zig new file mode 100644 index 00000000..225986dc --- /dev/null +++ b/src/core/examples/textured-quad/main.zig @@ -0,0 +1,204 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const zigimg = @import("zigimg"); +const assets = @import("assets"); + +pub const App = @This(); + +const Vertex = extern struct { + pos: @Vector(2, f32), + uv: @Vector(2, f32), +}; + +const vertices = [_]Vertex{ + .{ .pos = .{ -0.5, -0.5 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 0.5, -0.5 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 0.5, 0.5 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -0.5, 0.5 }, .uv = .{ 1, 0 } }, +}; +const index_data = [_]u32{ 0, 1, 2, 2, 3, 0 }; + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +title_timer: core.Timer, +timer: core.Timer, +pipeline: *gpu.RenderPipeline, +vertex_buffer: *gpu.Buffer, +index_buffer: *gpu.Buffer, +bind_group: *gpu.BindGroup, + +pub fn init(app: *App) !void { + try core.init(.{}); + const allocator = gpa.allocator(); + + const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); + + const vertex_attributes = [_]gpu.VertexAttribute{ + .{ .format = .float32x4, .offset = @offsetOf(Vertex, "pos"), .shader_location = 0 }, + .{ .format = .float32x2, .offset = @offsetOf(Vertex, "uv"), .shader_location = 1 }, + }; + const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{ + .array_stride = @sizeOf(Vertex), + .step_mode = .vertex, + .attributes = &vertex_attributes, + }); + + const blend = gpu.BlendState{}; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + .vertex = gpu.VertexState.init(.{ + .module = shader_module, + .entry_point = "vertex_main", + .buffers = &.{vertex_buffer_layout}, + }), + .primitive = .{ .cull_mode = .back }, + }; + const pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + shader_module.release(); + + const vertex_buffer = core.device.createBuffer(&.{ + .usage = .{ .vertex = true }, + .size = @sizeOf(Vertex) * vertices.len, + .mapped_at_creation = .true, + }); + const vertex_mapped = vertex_buffer.getMappedRange(Vertex, 0, vertices.len); + @memcpy(vertex_mapped.?, vertices[0..]); + vertex_buffer.unmap(); + + const index_buffer = core.device.createBuffer(&.{ + .usage = .{ .index = true }, + .size = @sizeOf(u32) * index_data.len, + .mapped_at_creation = .true, + }); + const index_mapped = index_buffer.getMappedRange(u32, 0, index_data.len); + @memcpy(index_mapped.?, index_data[0..]); + index_buffer.unmap(); + + const sampler = core.device.createSampler(&.{ .mag_filter = .linear, .min_filter = .linear }); + const queue = core.queue; + var img = try zigimg.Image.fromMemory(allocator, assets.gotta_go_fast_png); + defer img.deinit(); + const img_size = gpu.Extent3D{ + .width = @as(u32, @intCast(img.width)), + .height = @as(u32, @intCast(img.height)), + }; + const texture = core.device.createTexture(&.{ + .size = img_size, + .format = .rgba8_unorm, + .usage = .{ + .texture_binding = true, + .copy_dst = true, + .render_attachment = true, + }, + }); + const data_layout = gpu.Texture.DataLayout{ + .bytes_per_row = @as(u32, @intCast(img.width * 4)), + .rows_per_image = @as(u32, @intCast(img.height)), + }; + switch (img.pixels) { + .rgba32 => |pixels| queue.writeTexture(&.{ .texture = texture }, &data_layout, &img_size, pixels), + .rgb24 => |pixels| { + const data = try rgb24ToRgba32(allocator, pixels); + defer data.deinit(allocator); + queue.writeTexture(&.{ .texture = texture }, &data_layout, &img_size, data.rgba32); + }, + else => @panic("unsupported image color format"), + } + + const texture_view = texture.createView(&gpu.TextureView.Descriptor{}); + texture.release(); + + const bind_group_layout = pipeline.getBindGroupLayout(0); + const bind_group = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bind_group_layout, + .entries = &.{ + gpu.BindGroup.Entry.sampler(0, sampler), + gpu.BindGroup.Entry.textureView(1, texture_view), + }, + }), + ); + sampler.release(); + texture_view.release(); + bind_group_layout.release(); + + app.timer = try core.Timer.start(); + app.title_timer = try core.Timer.start(); + app.pipeline = pipeline; + app.vertex_buffer = vertex_buffer; + app.index_buffer = index_buffer; + app.bind_group = bind_group; +} + +pub fn deinit(app: *App) void { + app.pipeline.release(); + app.vertex_buffer.release(); + app.index_buffer.release(); + app.bind_group.release(); + core.deinit(); + _ = gpa.deinit(); +} + +pub fn update(app: *App) !bool { + var iter = core.pollEvents(); + while (iter.next()) |event| if (event == .close) return true; + + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = .{ .r = 0, .g = 0, .b = 0, .a = 0.0 }, + .load_op = .clear, + .store_op = .store, + }; + + const encoder = core.device.createCommandEncoder(null); + const render_pass_info = gpu.RenderPassDescriptor.init(.{ .color_attachments = &.{color_attachment} }); + + const pass = encoder.beginRenderPass(&render_pass_info); + pass.setPipeline(app.pipeline); + pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len); + pass.setIndexBuffer(app.index_buffer, .uint32, 0, @sizeOf(u32) * index_data.len); + pass.setBindGroup(0, app.bind_group, &.{}); + pass.drawIndexed(index_data.len, 1, 0, 0, 0); + pass.end(); + pass.release(); + + var command = encoder.finish(null); + encoder.release(); + + const queue = core.queue; + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Textured Quad [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + return false; +} + +fn rgb24ToRgba32(allocator: std.mem.Allocator, in: []zigimg.color.Rgb24) !zigimg.color.PixelStorage { + const out = try zigimg.color.PixelStorage.init(allocator, .rgba32, in.len); + var i: usize = 0; + while (i < in.len) : (i += 1) { + out.rgba32[i] = zigimg.color.Rgba32{ .r = in[i].r, .g = in[i].g, .b = in[i].b, .a = 255 }; + } + return out; +} diff --git a/src/core/examples/textured-quad/shader.wgsl b/src/core/examples/textured-quad/shader.wgsl new file mode 100644 index 00000000..3328340b --- /dev/null +++ b/src/core/examples/textured-quad/shader.wgsl @@ -0,0 +1,21 @@ +struct VertexOutput { + @builtin(position) Position : vec4, + @location(0) fragUV : vec2, +}; + +@vertex +fn vertex_main(@location(0) position : vec2, + @location(1) uv : vec2) -> VertexOutput { + var output : VertexOutput; + output.Position = vec4(position, 0, 1); + output.fragUV = uv; + return output; +} + +@group(0) @binding(0) var mySampler: sampler; +@group(0) @binding(1) var myTexture: texture_2d; + +@fragment +fn frag_main(@location(0) fragUV: vec2) -> @location(0) vec4 { + return textureSample(myTexture, mySampler, fragUV); +} \ No newline at end of file diff --git a/src/core/examples/triangle-msaa/main.zig b/src/core/examples/triangle-msaa/main.zig new file mode 100644 index 00000000..d28103ed --- /dev/null +++ b/src/core/examples/triangle-msaa/main.zig @@ -0,0 +1,128 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; + +pub const App = @This(); + +title_timer: core.Timer, +pipeline: *gpu.RenderPipeline, +texture: *gpu.Texture, +texture_view: *gpu.TextureView, + +const sample_count = 4; + +pub fn init(app: *App) !void { + try core.init(.{}); + app.title_timer = try core.Timer.start(); + + const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); + defer shader_module.release(); + + // Fragment state + const blend = gpu.BlendState{}; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + .vertex = gpu.VertexState{ + .module = shader_module, + .entry_point = "vertex_main", + }, + .multisample = gpu.MultisampleState{ + .count = sample_count, + }, + }; + + app.pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + + app.texture = core.device.createTexture(&gpu.Texture.Descriptor{ + .size = gpu.Extent3D{ + .width = core.descriptor.width, + .height = core.descriptor.height, + }, + .sample_count = sample_count, + .format = core.descriptor.format, + .usage = .{ .render_attachment = true }, + }); + app.texture_view = app.texture.createView(null); +} + +pub fn deinit(app: *App) void { + defer core.deinit(); + + app.pipeline.release(); + app.texture.release(); + app.texture_view.release(); +} + +pub fn update(app: *App) !bool { + var iter = core.pollEvents(); + while (iter.next()) |event| { + switch (event) { + .framebuffer_resize => |size| { + app.texture.release(); + app.texture = core.device.createTexture(&gpu.Texture.Descriptor{ + .size = gpu.Extent3D{ + .width = size.width, + .height = size.height, + }, + .sample_count = sample_count, + .format = core.descriptor.format, + .usage = .{ .render_attachment = true }, + }); + + app.texture_view.release(); + app.texture_view = app.texture.createView(null); + }, + .close => return true, + else => {}, + } + } + + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const color_attachment = gpu.RenderPassColorAttachment{ + .view = app.texture_view, + .resolve_target = back_buffer_view, + .clear_value = std.mem.zeroes(gpu.Color), + .load_op = .clear, + .store_op = .discard, + }; + + const encoder = core.device.createCommandEncoder(null); + const render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + }); + const pass = encoder.beginRenderPass(&render_pass_info); + pass.setPipeline(app.pipeline); + pass.draw(3, 1, 0, 0); + pass.end(); + pass.release(); + + var command = encoder.finish(null); + encoder.release(); + + const queue = core.queue; + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Triangle MSAA [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} diff --git a/src/core/examples/triangle-msaa/shader.wgsl b/src/core/examples/triangle-msaa/shader.wgsl new file mode 100644 index 00000000..429d87e0 --- /dev/null +++ b/src/core/examples/triangle-msaa/shader.wgsl @@ -0,0 +1,14 @@ +@vertex fn vertex_main( + @builtin(vertex_index) VertexIndex : u32 +) -> @builtin(position) vec4 { + var pos = array, 3>( + vec2( 0.0, 0.5), + vec2(-0.5, -0.5), + vec2( 0.5, -0.5) + ); + return vec4(pos[VertexIndex], 0.0, 1.0); +} + +@fragment fn frag_main() -> @location(0) vec4 { + return vec4(1.0, 0.0, 0.0, 1.0); +} diff --git a/src/core/examples/triangle/main.zig b/src/core/examples/triangle/main.zig new file mode 100644 index 00000000..a348e9f2 --- /dev/null +++ b/src/core/examples/triangle/main.zig @@ -0,0 +1,91 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; + +pub const App = @This(); + +title_timer: core.Timer, +pipeline: *gpu.RenderPipeline, + +pub fn init(app: *App) !void { + try core.init(.{}); + + const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); + defer shader_module.release(); + + // Fragment state + const blend = gpu.BlendState{}; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + .vertex = gpu.VertexState{ + .module = shader_module, + .entry_point = "vertex_main", + }, + }; + const pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + + app.* = .{ .title_timer = try core.Timer.start(), .pipeline = pipeline }; +} + +pub fn deinit(app: *App) void { + defer core.deinit(); + app.pipeline.release(); +} + +pub fn update(app: *App) !bool { + var iter = core.pollEvents(); + while (iter.next()) |event| { + switch (event) { + .close => return true, + else => {}, + } + } + + const queue = core.queue; + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = std.mem.zeroes(gpu.Color), + .load_op = .clear, + .store_op = .store, + }; + + const encoder = core.device.createCommandEncoder(null); + const render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + }); + const pass = encoder.beginRenderPass(&render_pass_info); + pass.setPipeline(app.pipeline); + pass.draw(3, 1, 0, 0); + pass.end(); + pass.release(); + + var command = encoder.finish(null); + encoder.release(); + + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Triangle [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} diff --git a/src/core/examples/triangle/shader.wgsl b/src/core/examples/triangle/shader.wgsl new file mode 100644 index 00000000..429d87e0 --- /dev/null +++ b/src/core/examples/triangle/shader.wgsl @@ -0,0 +1,14 @@ +@vertex fn vertex_main( + @builtin(vertex_index) VertexIndex : u32 +) -> @builtin(position) vec4 { + var pos = array, 3>( + vec2( 0.0, 0.5), + vec2(-0.5, -0.5), + vec2( 0.5, -0.5) + ); + return vec4(pos[VertexIndex], 0.0, 1.0); +} + +@fragment fn frag_main() -> @location(0) vec4 { + return vec4(1.0, 0.0, 0.0, 1.0); +} diff --git a/src/core/examples/two-cubes/cube_mesh.zig b/src/core/examples/two-cubes/cube_mesh.zig new file mode 100644 index 00000000..f26c75ac --- /dev/null +++ b/src/core/examples/two-cubes/cube_mesh.zig @@ -0,0 +1,49 @@ +pub const Vertex = extern struct { + pos: @Vector(4, f32), + col: @Vector(4, f32), + uv: @Vector(2, f32), +}; + +pub const vertices = [_]Vertex{ + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, + + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } }, + + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, + .{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } }, +}; diff --git a/src/core/examples/two-cubes/main.zig b/src/core/examples/two-cubes/main.zig new file mode 100755 index 00000000..4f9a95f5 --- /dev/null +++ b/src/core/examples/two-cubes/main.zig @@ -0,0 +1,223 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; +const zm = @import("zmath"); +const Vertex = @import("cube_mesh.zig").Vertex; +const vertices = @import("cube_mesh.zig").vertices; + +const UniformBufferObject = struct { + mat: zm.Mat, +}; + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +title_timer: core.Timer, +timer: core.Timer, +pipeline: *gpu.RenderPipeline, +vertex_buffer: *gpu.Buffer, +uniform_buffer: *gpu.Buffer, +bind_group1: *gpu.BindGroup, +bind_group2: *gpu.BindGroup, + +pub const App = @This(); + +pub fn init(app: *App) !void { + try core.init(.{}); + app.title_timer = try core.Timer.start(); + app.timer = try core.Timer.start(); + + const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); + + const vertex_attributes = [_]gpu.VertexAttribute{ + .{ .format = .float32x4, .offset = @offsetOf(Vertex, "pos"), .shader_location = 0 }, + .{ .format = .float32x2, .offset = @offsetOf(Vertex, "uv"), .shader_location = 1 }, + }; + const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{ + .array_stride = @sizeOf(Vertex), + .step_mode = .vertex, + .attributes = &vertex_attributes, + }); + + const blend = gpu.BlendState{}; + const color_target = gpu.ColorTargetState{ + .format = core.descriptor.format, + .blend = &blend, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + + const bgle = gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, true, 0); + const bgl = core.device.createBindGroupLayout( + &gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &.{bgle}, + }), + ); + + const bind_group_layouts = [_]*gpu.BindGroupLayout{bgl}; + const pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &bind_group_layouts, + })); + + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .fragment = &fragment, + .layout = pipeline_layout, + .vertex = gpu.VertexState.init(.{ + .module = shader_module, + .entry_point = "vertex_main", + .buffers = &.{vertex_buffer_layout}, + }), + .primitive = .{ + .cull_mode = .back, + }, + }; + + const vertex_buffer = core.device.createBuffer(&.{ + .usage = .{ .vertex = true }, + .size = @sizeOf(Vertex) * vertices.len, + .mapped_at_creation = .true, + }); + const vertex_mapped = vertex_buffer.getMappedRange(Vertex, 0, vertices.len); + @memcpy(vertex_mapped.?, vertices[0..]); + vertex_buffer.unmap(); + + // uniformBindGroup offset must be 256-byte aligned + const uniform_offset = 256; + const uniform_buffer = core.device.createBuffer(&.{ + .usage = .{ .uniform = true, .copy_dst = true }, + .size = @sizeOf(UniformBufferObject) + uniform_offset, + .mapped_at_creation = .false, + }); + + const bind_group1 = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bgl, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, uniform_buffer, 0, @sizeOf(UniformBufferObject)), + }, + }), + ); + + const bind_group2 = core.device.createBindGroup( + &gpu.BindGroup.Descriptor.init(.{ + .layout = bgl, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, uniform_buffer, uniform_offset, @sizeOf(UniformBufferObject)), + }, + }), + ); + + app.pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + app.vertex_buffer = vertex_buffer; + app.uniform_buffer = uniform_buffer; + app.bind_group1 = bind_group1; + app.bind_group2 = bind_group2; + + shader_module.release(); + pipeline_layout.release(); + bgl.release(); +} + +pub fn deinit(app: *App) void { + defer _ = gpa.deinit(); + defer core.deinit(); + + app.pipeline.release(); + app.vertex_buffer.release(); + app.uniform_buffer.release(); + app.bind_group1.release(); + app.bind_group2.release(); +} + +pub fn update(app: *App) !bool { + var iter = core.pollEvents(); + while (iter.next()) |event| { + switch (event) { + .key_press => |ev| { + if (ev.key == .space) return true; + }, + .close => return true, + else => {}, + } + } + + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .clear_value = std.mem.zeroes(gpu.Color), + .load_op = .clear, + .store_op = .store, + }; + + const encoder = core.device.createCommandEncoder(null); + const render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + }); + + { + const time = app.timer.read(); + const rotation1 = zm.mul(zm.rotationX(time * (std.math.pi / 2.0)), zm.rotationZ(time * (std.math.pi / 2.0))); + const rotation2 = zm.mul(zm.rotationZ(time * (std.math.pi / 2.0)), zm.rotationX(time * (std.math.pi / 2.0))); + const model1 = zm.mul(rotation1, zm.translation(-2, 0, 0)); + const model2 = zm.mul(rotation2, zm.translation(2, 0, 0)); + const view = zm.lookAtRh( + zm.Vec{ 0, -4, 2, 1 }, + zm.Vec{ 0, 0, 0, 1 }, + zm.Vec{ 0, 0, 1, 0 }, + ); + const proj = zm.perspectiveFovRh( + (2.0 * std.math.pi / 5.0), + @as(f32, @floatFromInt(core.descriptor.width)) / @as(f32, @floatFromInt(core.descriptor.height)), + 1, + 100, + ); + const mvp1 = zm.mul(zm.mul(model1, view), proj); + const mvp2 = zm.mul(zm.mul(model2, view), proj); + const ubo1 = UniformBufferObject{ + .mat = zm.transpose(mvp1), + }; + const ubo2 = UniformBufferObject{ + .mat = zm.transpose(mvp2), + }; + + encoder.writeBuffer(app.uniform_buffer, 0, &[_]UniformBufferObject{ubo1}); + + // bind_group2 offset + encoder.writeBuffer(app.uniform_buffer, 256, &[_]UniformBufferObject{ubo2}); + } + + const pass = encoder.beginRenderPass(&render_pass_info); + pass.setPipeline(app.pipeline); + pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len); + + pass.setBindGroup(0, app.bind_group1, &.{0}); + pass.draw(vertices.len, 1, 0, 0); + pass.setBindGroup(0, app.bind_group2, &.{0}); + pass.draw(vertices.len, 1, 0, 0); + + pass.end(); + pass.release(); + + var command = encoder.finish(null); + encoder.release(); + + const queue = core.queue; + queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + core.swap_chain.present(); + back_buffer_view.release(); + + // update the window title every second + if (app.title_timer.read() >= 1.0) { + app.title_timer.reset(); + try core.printTitle("Two Cubes [ {d}fps ] [ Input {d}hz ]", .{ + core.frameRate(), + core.inputRate(), + }); + } + + return false; +} diff --git a/src/core/examples/two-cubes/shader.wgsl b/src/core/examples/two-cubes/shader.wgsl new file mode 100644 index 00000000..05384f41 --- /dev/null +++ b/src/core/examples/two-cubes/shader.wgsl @@ -0,0 +1,24 @@ +@group(0) @binding(0) var ubo : mat4x4; +struct VertexOut { + @builtin(position) position_clip : vec4, + @location(0) fragUV : vec2, + @location(1) fragPosition: vec4, +} + +@vertex fn vertex_main( + @location(0) position : vec4, + @location(1) uv: vec2 +) -> VertexOut { + var output : VertexOut; + output.position_clip = position * ubo; + output.fragUV = uv; + output.fragPosition = 0.5 * (position + vec4(1.0, 1.0, 1.0, 1.0)); + return output; +} + +@fragment fn frag_main( + @location(0) fragUV: vec2, + @location(1) fragPosition: vec4 +) -> @location(0) vec4 { + return fragPosition; +} diff --git a/src/core/examples/wasm-test/main.zig b/src/core/examples/wasm-test/main.zig new file mode 100644 index 00000000..aaa3a640 --- /dev/null +++ b/src/core/examples/wasm-test/main.zig @@ -0,0 +1,35 @@ +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; + +pub const App = @This(); + +pub fn init(app: *App) !void { + try core.init(.{}); + app.* = App{}; +} + +pub fn deinit(app: *App) void { + defer core.deinit(); + _ = app; +} + +pub fn update(app: *App) !bool { + _ = app; + var iter = core.pollEvents(); + while (iter.next()) |event| { + std.log.debug("event: {}\n", .{event}); + switch (event) { + .close => return true, + .key_press => |ev| { + if (ev.key == .p) @panic("p pressed, panic triggered"); + if (ev.key == .q) { + std.log.err("q pressed, exiting app", .{}); + return true; + } + }, + else => {}, + } + } + return false; +} diff --git a/src/core/examples/zmath.zig b/src/core/examples/zmath.zig new file mode 100644 index 00000000..7aa6bf54 --- /dev/null +++ b/src/core/examples/zmath.zig @@ -0,0 +1,996 @@ +//! Heavily stripped down zmath version. +//! +//! In your own projects, you should bring your own math library. This file is just for example +//! purposes. +//! + +const builtin = @import("builtin"); +const std = @import("std"); +const math = std.math; +const assert = std.debug.assert; +const expect = std.testing.expect; + +pub const Vec = @Vector(4, f32); +pub const Mat = [4]@Vector(4, f32); +pub const Quat = @Vector(4, f32); + +pub inline fn abs(v: anytype) @TypeOf(v) { + return @abs(v); +} + +inline fn dot3(v0: Vec, v1: Vec) Vec { + const dot = v0 * v1; + return @splat(dot[0] + dot[1] + dot[2]); +} + +pub inline fn normalize3(v: Vec) Vec { + return v * @as(Vec, @splat(1.0)) / @sqrt(dot3(v, v)); +} + +pub fn scaling(x: f32, y: f32, z: f32) Mat { + return .{ + .{ x, 0.0, 0.0, 0.0 }, + .{ 0.0, y, 0.0, 0.0 }, + .{ 0.0, 0.0, z, 0.0 }, + .{ 0.0, 0.0, 0.0, 1.0 }, + }; +} + +pub inline fn cross3(a: Vec, b: Vec) Vec { + const v1 = Vec{ a[1], a[2], a[0], 1.0 }; + const v2 = Vec{ b[2], b[0], b[1], 1.0 }; + const sub1 = v1 * v2; + + const _v1 = Vec{ a[2], a[0], a[1], 1.0 }; + const _v2 = Vec{ b[1], b[2], b[0], 1.0 }; + const sub2 = _v1 * _v2; + + return sub1 - sub2; +} + +pub fn orthographicRh(w: f32, h: f32, near: f32, far: f32) Mat { + assert(!math.approxEqAbs(f32, w, 0.0, 0.001)); + assert(!math.approxEqAbs(f32, h, 0.0, 0.001)); + assert(!math.approxEqAbs(f32, far, near, 0.001)); + + const r = 1 / (near - far); + return .{ + .{ 2 / w, 0.0, 0.0, 0.0 }, + .{ 0.0, 2 / h, 0.0, 0.0 }, + .{ 0.0, 0.0, r, 0.0 }, + .{ 0.0, 0.0, r * near, 1.0 }, + }; +} + +//--- +// Public APIs used by examples that we should reduce to their minimal counterparts +//--- +pub fn lookAtRh(eyepos: Vec, focuspos: Vec, updir: Vec) Mat { + return lookToLh(eyepos, eyepos - focuspos, updir); +} +pub inline fn storeMat(mem: []f32, m: Mat) void { + store(mem[0..4], m[0], 0); + store(mem[4..8], m[1], 0); + store(mem[8..12], m[2], 0); + store(mem[12..16], m[3], 0); +} +pub fn quatFromAxisAngle(axis: Vec, angle: f32) Quat { + assert(!all(axis == splat(F32x4, 0.0), 3)); + assert(!all(isInf(axis), 3)); + const normal = normalize3(axis); + return quatFromNormAxisAngle(normal, angle); +} +pub fn perspectiveFovRh(fovy: f32, aspect: f32, near: f32, far: f32) Mat { + const scfov = sincos(0.5 * fovy); + + assert(near > 0.0 and far > 0.0); + assert(!math.approxEqAbs(f32, scfov[0], 0.0, 0.001)); + assert(!math.approxEqAbs(f32, far, near, 0.001)); + assert(!math.approxEqAbs(f32, aspect, 0.0, 0.01)); + + const h = scfov[1] / scfov[0]; + const w = h / aspect; + const r = far / (near - far); + return .{ + f32x4(w, 0.0, 0.0, 0.0), + f32x4(0.0, h, 0.0, 0.0), + f32x4(0.0, 0.0, r, -1.0), + f32x4(0.0, 0.0, r * near, 0.0), + }; +} +pub fn qmul(q0: Quat, q1: Quat) Quat { + var result = swizzle(q1, .w, .w, .w, .w); + var q1x = swizzle(q1, .x, .x, .x, .x); + var q1y = swizzle(q1, .y, .y, .y, .y); + var q1z = swizzle(q1, .z, .z, .z, .z); + result = result * q0; + var q0_shuf = swizzle(q0, .w, .z, .y, .x); + q1x = q1x * q0_shuf; + q0_shuf = swizzle(q0_shuf, .y, .x, .w, .z); + result = mulAdd(q1x, f32x4(1.0, -1.0, 1.0, -1.0), result); + q1y = q1y * q0_shuf; + q0_shuf = swizzle(q0_shuf, .w, .z, .y, .x); + q1y = q1y * f32x4(1.0, 1.0, -1.0, -1.0); + q1z = q1z * q0_shuf; + q1y = mulAdd(q1z, f32x4(-1.0, 1.0, 1.0, -1.0), q1y); + return result + q1y; +} +pub fn mul(a: anytype, b: anytype) mulRetType(@TypeOf(a), @TypeOf(b)) { + const Ta = @TypeOf(a); + const Tb = @TypeOf(b); + if (Ta == Mat and Tb == Mat) { + return mulMat(a, b); + } else if (Ta == f32 and Tb == Mat) { + const va = splat(F32x4, a); + return Mat{ va * b[0], va * b[1], va * b[2], va * b[3] }; + } else if (Ta == Mat and Tb == f32) { + const vb = splat(F32x4, b); + return Mat{ a[0] * vb, a[1] * vb, a[2] * vb, a[3] * vb }; + } else if (Ta == Vec and Tb == Mat) { + return vecMulMat(a, b); + } else if (Ta == Mat and Tb == Vec) { + return matMulVec(a, b); + } else { + @compileError("zmath.mul() not implemented for types: " ++ @typeName(Ta) ++ ", " ++ @typeName(Tb)); + } +} +pub fn translationV(v: Vec) Mat { + return translation(v[0], v[1], v[2]); +} +pub fn transpose(m: Mat) Mat { + const temp1 = @shuffle(f32, m[0], m[1], [4]i32{ 0, 1, ~@as(i32, 0), ~@as(i32, 1) }); + const temp3 = @shuffle(f32, m[0], m[1], [4]i32{ 2, 3, ~@as(i32, 2), ~@as(i32, 3) }); + const temp2 = @shuffle(f32, m[2], m[3], [4]i32{ 0, 1, ~@as(i32, 0), ~@as(i32, 1) }); + const temp4 = @shuffle(f32, m[2], m[3], [4]i32{ 2, 3, ~@as(i32, 2), ~@as(i32, 3) }); + return .{ + @shuffle(f32, temp1, temp2, [4]i32{ 0, 2, ~@as(i32, 0), ~@as(i32, 2) }), + @shuffle(f32, temp1, temp2, [4]i32{ 1, 3, ~@as(i32, 1), ~@as(i32, 3) }), + @shuffle(f32, temp3, temp4, [4]i32{ 0, 2, ~@as(i32, 0), ~@as(i32, 2) }), + @shuffle(f32, temp3, temp4, [4]i32{ 1, 3, ~@as(i32, 1), ~@as(i32, 3) }), + }; +} +pub fn rotationX(angle: f32) Mat { + const sc = sincos(angle); + return .{ + f32x4(1.0, 0.0, 0.0, 0.0), + f32x4(0.0, sc[1], sc[0], 0.0), + f32x4(0.0, -sc[0], sc[1], 0.0), + f32x4(0.0, 0.0, 0.0, 1.0), + }; +} +pub fn rotationY(angle: f32) Mat { + const sc = sincos(angle); + return .{ + f32x4(sc[1], 0.0, -sc[0], 0.0), + f32x4(0.0, 1.0, 0.0, 0.0), + f32x4(sc[0], 0.0, sc[1], 0.0), + f32x4(0.0, 0.0, 0.0, 1.0), + }; +} +pub fn rotationZ(angle: f32) Mat { + const sc = sincos(angle); + return .{ + f32x4(sc[1], sc[0], 0.0, 0.0), + f32x4(-sc[0], sc[1], 0.0, 0.0), + f32x4(0.0, 0.0, 1.0, 0.0), + f32x4(0.0, 0.0, 0.0, 1.0), + }; +} +pub fn cos(v: anytype) @TypeOf(v) { + const T = @TypeOf(v); + return switch (T) { + f32 => cos32(v), + F32x4 => cos32xN(v), + else => @compileError("zmath.cos() not implemented for " ++ @typeName(T)), + }; +} +pub fn sin(v: anytype) @TypeOf(v) { + const T = @TypeOf(v); + return switch (T) { + f32 => sin32(v), + F32x4 => sin32xN(v), + else => @compileError("zmath.sin() not implemented for " ++ @typeName(T)), + }; +} +// Produces Z values in [-1.0, 1.0] range (OpenGL defaults) +pub fn perspectiveFovRhGl(fovy: f32, aspect: f32, near: f32, far: f32) Mat { + const scfov = sincos(0.5 * fovy); + + assert(near > 0.0 and far > 0.0); + assert(!math.approxEqAbs(f32, scfov[0], 0.0, 0.001)); + assert(!math.approxEqAbs(f32, far, near, 0.001)); + assert(!math.approxEqAbs(f32, aspect, 0.0, 0.01)); + + const h = scfov[1] / scfov[0]; + const w = h / aspect; + const r = near - far; + return .{ + f32x4(w, 0.0, 0.0, 0.0), + f32x4(0.0, h, 0.0, 0.0), + f32x4(0.0, 0.0, (near + far) / r, -1.0), + f32x4(0.0, 0.0, 2.0 * near * far / r, 0.0), + }; +} +pub inline fn clamp(v: anytype, vmin: anytype, vmax: anytype) @TypeOf(v, vmin, vmax) { + var result = @max(vmin, v); + result = min(vmax, result); + return result; +} +pub fn inverse(a: anytype) @TypeOf(a) { + const T = @TypeOf(a); + return switch (T) { + Mat => inverseMat(a), + else => @compileError("zmath.inverse() not implemented for " ++ @typeName(T)), + }; +} +pub fn identity() Mat { + const static = struct { + const identity = Mat{ + f32x4(1.0, 0.0, 0.0, 0.0), + f32x4(0.0, 1.0, 0.0, 0.0), + f32x4(0.0, 0.0, 1.0, 0.0), + f32x4(0.0, 0.0, 0.0, 1.0), + }; + }; + return static.identity; +} +pub fn translation(x: f32, y: f32, z: f32) Mat { + return .{ + f32x4(1.0, 0.0, 0.0, 0.0), + f32x4(0.0, 1.0, 0.0, 0.0), + f32x4(0.0, 0.0, 1.0, 0.0), + f32x4(x, y, z, 1.0), + }; +} + +//--- +// Internal APIs +//--- + +inline fn f32x4(e0: f32, e1: f32, e2: f32, e3: f32) F32x4 { + return .{ e0, e1, e2, e3 }; +} + +const F32x4 = @Vector(4, f32); + +inline fn veclen(comptime T: type) comptime_int { + return @typeInfo(T).Vector.len; +} + +inline fn splat(comptime T: type, value: f32) T { + return @splat(value); +} +inline fn splatInt(comptime T: type, value: u32) T { + return @splat(@bitCast(value)); +} + +fn load(mem: []const f32, comptime T: type, comptime len: u32) T { + var v = splat(T, 0.0); + const loop_len = if (len == 0) veclen(T) else len; + comptime var i: u32 = 0; + inline while (i < loop_len) : (i += 1) { + v[i] = mem[i]; + } + return v; +} + +fn store(mem: []f32, v: anytype, comptime len: u32) void { + const T = @TypeOf(v); + const loop_len = if (len == 0) veclen(T) else len; + comptime var i: u32 = 0; + inline while (i < loop_len) : (i += 1) { + mem[i] = v[i]; + } +} + +inline fn loadArr2(arr: [2]f32) F32x4 { + return f32x4(arr[0], arr[1], 0.0, 0.0); +} +inline fn loadArr2zw(arr: [2]f32, z: f32, w: f32) F32x4 { + return f32x4(arr[0], arr[1], z, w); +} +inline fn loadArr3(arr: [3]f32) F32x4 { + return f32x4(arr[0], arr[1], arr[2], 0.0); +} +inline fn loadArr3w(arr: [3]f32, w: f32) F32x4 { + return f32x4(arr[0], arr[1], arr[2], w); +} +inline fn loadArr4(arr: [4]f32) F32x4 { + return f32x4(arr[0], arr[1], arr[2], arr[3]); +} + +inline fn storeArr2(arr: *[2]f32, v: F32x4) void { + arr.* = .{ v[0], v[1] }; +} +inline fn storeArr3(arr: *[3]f32, v: F32x4) void { + arr.* = .{ v[0], v[1], v[2] }; +} + +inline fn arr3Ptr(ptr: anytype) *const [3]f32 { + comptime assert(@typeInfo(@TypeOf(ptr)) == .Pointer); + const T = std.meta.Child(@TypeOf(ptr)); + comptime assert(T == F32x4); + return @as(*const [3]f32, @ptrCast(ptr)); +} + +inline fn arrNPtr(ptr: anytype) [*]const f32 { + comptime assert(@typeInfo(@TypeOf(ptr)) == .Pointer); + const T = std.meta.Child(@TypeOf(ptr)); + comptime assert(T == Mat or T == F32x4); + return @as([*]const f32, @ptrCast(ptr)); +} + +inline fn vecToArr2(v: Vec) [2]f32 { + return .{ v[0], v[1] }; +} +inline fn vecToArr3(v: Vec) [3]f32 { + return .{ v[0], v[1], v[2] }; +} +inline fn vecToArr4(v: Vec) [4]f32 { + return .{ v[0], v[1], v[2], v[3] }; +} + +fn all(vb: anytype, comptime len: u32) bool { + const T = @TypeOf(vb); + if (len > veclen(T)) { + @compileError("zmath.all(): 'len' is greater than vector len of type " ++ @typeName(T)); + } + const loop_len = if (len == 0) veclen(T) else len; + const ab: [veclen(T)]bool = vb; + comptime var i: u32 = 0; + var result = true; + inline while (i < loop_len) : (i += 1) { + result = result and ab[i]; + } + return result; +} + +fn any(vb: anytype, comptime len: u32) bool { + const T = @TypeOf(vb); + if (len > veclen(T)) { + @compileError("zmath.any(): 'len' is greater than vector len of type " ++ @typeName(T)); + } + const loop_len = if (len == 0) veclen(T) else len; + const ab: [veclen(T)]bool = vb; + comptime var i: u32 = 0; + var result = false; + inline while (i < loop_len) : (i += 1) { + result = result or ab[i]; + } + return result; +} + +inline fn isNearEqual( + v0: anytype, + v1: anytype, + epsilon: anytype, +) @Vector(veclen(@TypeOf(v0)), bool) { + const T = @TypeOf(v0, v1, epsilon); + const delta = v0 - v1; + const temp = maxFast(delta, splat(T, 0.0) - delta); + return temp <= epsilon; +} + +inline fn isNan( + v: anytype, +) @Vector(veclen(@TypeOf(v)), bool) { + return v != v; +} + +inline fn isInf( + v: anytype, +) @Vector(veclen(@TypeOf(v)), bool) { + const T = @TypeOf(v); + return abs(v) == splat(T, math.inf(f32)); +} + +inline fn isInBounds( + v: anytype, + bounds: anytype, +) @Vector(veclen(@TypeOf(v)), bool) { + const T = @TypeOf(v, bounds); + const Tu = @Vector(veclen(T), u1); + const Tr = @Vector(veclen(T), bool); + + // 2 x cmpleps, xorps, load, andps + const b0 = v <= bounds; + const b1 = (bounds * splat(T, -1.0)) <= v; + const b0u = @as(Tu, @bitCast(b0)); + const b1u = @as(Tu, @bitCast(b1)); + return @as(Tr, @bitCast(b0u & b1u)); +} + +inline fn andInt(v0: anytype, v1: anytype) @TypeOf(v0, v1) { + const T = @TypeOf(v0, v1); + const Tu = @Vector(veclen(T), u32); + const v0u = @as(Tu, @bitCast(v0)); + const v1u = @as(Tu, @bitCast(v1)); + return @as(T, @bitCast(v0u & v1u)); // andps +} + +inline fn andNotInt(v0: anytype, v1: anytype) @TypeOf(v0, v1) { + const T = @TypeOf(v0, v1); + const Tu = @Vector(veclen(T), u32); + const v0u = @as(Tu, @bitCast(v0)); + const v1u = @as(Tu, @bitCast(v1)); + return @as(T, @bitCast(~v0u & v1u)); // andnps +} + +inline fn orInt(v0: anytype, v1: anytype) @TypeOf(v0, v1) { + const T = @TypeOf(v0, v1); + const Tu = @Vector(veclen(T), u32); + const v0u = @as(Tu, @bitCast(v0)); + const v1u = @as(Tu, @bitCast(v1)); + return @as(T, @bitCast(v0u | v1u)); // orps +} + +inline fn norInt(v0: anytype, v1: anytype) @TypeOf(v0, v1) { + const T = @TypeOf(v0, v1); + const Tu = @Vector(veclen(T), u32); + const v0u = @as(Tu, @bitCast(v0)); + const v1u = @as(Tu, @bitCast(v1)); + return @as(T, @bitCast(~(v0u | v1u))); // por, pcmpeqd, pxor +} + +inline fn xorInt(v0: anytype, v1: anytype) @TypeOf(v0, v1) { + const T = @TypeOf(v0, v1); + const Tu = @Vector(veclen(T), u32); + const v0u = @as(Tu, @bitCast(v0)); + const v1u = @as(Tu, @bitCast(v1)); + return @as(T, @bitCast(v0u ^ v1u)); // xorps +} + +inline fn minFast(v0: anytype, v1: anytype) @TypeOf(v0, v1) { + return select(v0 < v1, v0, v1); // minps +} + +inline fn maxFast(v0: anytype, v1: anytype) @TypeOf(v0, v1) { + return select(v0 > v1, v0, v1); // maxps +} + +inline fn min(v0: anytype, v1: anytype) @TypeOf(v0, v1) { + // This will handle inf & nan + return @min(v0, v1); // minps, cmpunordps, andps, andnps, orps +} + +fn round(v: anytype) @TypeOf(v) { + const T = @TypeOf(v); + const sign = andInt(v, splatNegativeZero(T)); + const magic = orInt(splatNoFraction(T), sign); + var r1 = v + magic; + r1 = r1 - magic; + const r2 = abs(v); + const mask = r2 <= splatNoFraction(T); + return select(mask, r1, v); +} + +fn trunc(v: anytype) @TypeOf(v) { + const T = @TypeOf(v); + const mask = abs(v) < splatNoFraction(T); + const result = floatToIntAndBack(v); + return select(mask, result, v); +} + +fn floor(v: anytype) @TypeOf(v) { + const T = @TypeOf(v); + const mask = abs(v) < splatNoFraction(T); + var result = floatToIntAndBack(v); + const larger_mask = result > v; + const larger = select(larger_mask, splat(T, -1.0), splat(T, 0.0)); + result = result + larger; + return select(mask, result, v); +} + +fn ceil(v: anytype) @TypeOf(v) { + const T = @TypeOf(v); + const mask = abs(v) < splatNoFraction(T); + var result = floatToIntAndBack(v); + const smaller_mask = result < v; + const smaller = select(smaller_mask, splat(T, -1.0), splat(T, 0.0)); + result = result - smaller; + return select(mask, result, v); +} + +inline fn clampFast(v: anytype, vmin: anytype, vmax: anytype) @TypeOf(v, vmin, vmax) { + var result = maxFast(vmin, v); + result = minFast(vmax, result); + return result; +} + +inline fn saturate(v: anytype) @TypeOf(v) { + const T = @TypeOf(v); + var result = @max(v, splat(T, 0.0)); + result = min(result, splat(T, 1.0)); + return result; +} + +inline fn saturateFast(v: anytype) @TypeOf(v) { + const T = @TypeOf(v); + var result = maxFast(v, splat(T, 0.0)); + result = minFast(result, splat(T, 1.0)); + return result; +} + +inline fn select(mask: anytype, v0: anytype, v1: anytype) @TypeOf(v0, v1) { + return @select(f32, mask, v0, v1); +} + +inline fn lerp(v0: anytype, v1: anytype, t: f32) @TypeOf(v0, v1) { + const T = @TypeOf(v0, v1); + return v0 + (v1 - v0) * splat(T, t); // subps, shufps, addps, mulps +} + +inline fn lerpV(v0: anytype, v1: anytype, t: anytype) @TypeOf(v0, v1, t) { + return v0 + (v1 - v0) * t; // subps, addps, mulps +} + +inline fn lerpInverse(v0: anytype, v1: anytype, t: anytype) @TypeOf(v0, v1) { + const T = @TypeOf(v0, v1); + return (splat(T, t) - v0) / (v1 - v0); +} + +inline fn lerpInverseV(v0: anytype, v1: anytype, t: anytype) @TypeOf(v0, v1, t) { + return (t - v0) / (v1 - v0); +} + +// Frame rate independent lerp (or "damp"), for approaching things over time. +// Reference: https://www.gamedeveloper.com/programming/improved-lerp-smoothing- +inline fn lerpOverTime(v0: anytype, v1: anytype, rate: anytype, dt: anytype) @TypeOf(v0, v1) { + const t = std.math.exp2(-rate * dt); + return lerp(v0, v1, t); +} + +inline fn lerpVOverTime(v0: anytype, v1: anytype, rate: anytype, dt: anytype) @TypeOf(v0, v1, rate, dt) { + const t = std.math.exp2(-rate * dt); + return lerpV(v0, v1, t); +} + +/// To transform a vector of values from one range to another. +inline fn mapLinear(v: anytype, min1: anytype, max1: anytype, min2: anytype, max2: anytype) @TypeOf(v) { + const T = @TypeOf(v); + const min1V = splat(T, min1); + const max1V = splat(T, max1); + const min2V = splat(T, min2); + const max2V = splat(T, max2); + const dV = max1V - min1V; + return min2V + (v - min1V) * (max2V - min2V) / dV; +} + +inline fn mapLinearV(v: anytype, min1: anytype, max1: anytype, min2: anytype, max2: anytype) @TypeOf(v, min1, max1, min2, max2) { + const d = max1 - min1; + return min2 + (v - min1) * (max2 - min2) / d; +} + +const F32x4Component = enum { x, y, z, w }; + +inline fn swizzle( + v: F32x4, + comptime x: F32x4Component, + comptime y: F32x4Component, + comptime z: F32x4Component, + comptime w: F32x4Component, +) F32x4 { + return @shuffle(f32, v, undefined, [4]i32{ @intFromEnum(x), @intFromEnum(y), @intFromEnum(z), @intFromEnum(w) }); +} + +inline fn mod(v0: anytype, v1: anytype) @TypeOf(v0, v1) { + // vdivps, vroundps, vmulps, vsubps + return v0 - v1 * trunc(v0 / v1); +} + +fn modAngle(v: anytype) @TypeOf(v) { + const T = @TypeOf(v); + return switch (T) { + f32 => modAngle32(v), + F32x4 => modAngle32xN(v), + else => @compileError("zmath.modAngle() not implemented for " ++ @typeName(T)), + }; +} + +inline fn modAngle32xN(v: anytype) @TypeOf(v) { + const T = @TypeOf(v); + return v - splat(T, math.tau) * round(v * splat(T, 1.0 / math.tau)); // 2 x vmulps, 2 x load, vroundps, vaddps +} + +inline fn mulAdd(v0: anytype, v1: anytype, v2: anytype) @TypeOf(v0, v1, v2) { + return v0 * v1 + v2; // Compiler will generate mul, add sequence (no fma even if the target supports it). +} + +fn sin32xN(v: anytype) @TypeOf(v) { + // 11-degree minimax approximation + const T = @TypeOf(v); + + var x = modAngle(v); + const sign = andInt(x, splatNegativeZero(T)); + const c = orInt(sign, splat(T, math.pi)); + const absx = andNotInt(sign, x); + const rflx = c - x; + const comp = absx <= splat(T, 0.5 * math.pi); + x = select(comp, x, rflx); + const x2 = x * x; + + var result = mulAdd(splat(T, -2.3889859e-08), x2, splat(T, 2.7525562e-06)); + result = mulAdd(result, x2, splat(T, -0.00019840874)); + result = mulAdd(result, x2, splat(T, 0.0083333310)); + result = mulAdd(result, x2, splat(T, -0.16666667)); + result = mulAdd(result, x2, splat(T, 1.0)); + return x * result; +} + +fn cos32xN(v: anytype) @TypeOf(v) { + // 10-degree minimax approximation + const T = @TypeOf(v); + + var x = modAngle(v); + var sign = andInt(x, splatNegativeZero(T)); + const c = orInt(sign, splat(T, math.pi)); + const absx = andNotInt(sign, x); + const rflx = c - x; + const comp = absx <= splat(T, 0.5 * math.pi); + x = select(comp, x, rflx); + sign = select(comp, splat(T, 1.0), splat(T, -1.0)); + const x2 = x * x; + + var result = mulAdd(splat(T, -2.6051615e-07), x2, splat(T, 2.4760495e-05)); + result = mulAdd(result, x2, splat(T, -0.0013888378)); + result = mulAdd(result, x2, splat(T, 0.041666638)); + result = mulAdd(result, x2, splat(T, -0.5)); + result = mulAdd(result, x2, splat(T, 1.0)); + return sign * result; +} + +fn sincos(v: anytype) [2]@TypeOf(v) { + const T = @TypeOf(v); + return switch (T) { + f32 => sincos32(v), + else => @compileError("zmath.sincos() not implemented for " ++ @typeName(T)), + }; +} + +inline fn dot2(v0: Vec, v1: Vec) F32x4 { + var xmm0 = v0 * v1; // | x0*x1 | y0*y1 | -- | -- | + const xmm1 = swizzle(xmm0, .y, .x, .x, .x); // | y0*y1 | -- | -- | -- | + xmm0 = f32x4(xmm0[0] + xmm1[0], xmm0[1], xmm0[2], xmm0[3]); // | x0*x1 + y0*y1 | -- | -- | -- | + return swizzle(xmm0, .x, .x, .x, .x); +} + +inline fn dot4(v0: Vec, v1: Vec) F32x4 { + var xmm0 = v0 * v1; // | x0*x1 | y0*y1 | z0*z1 | w0*w1 | + var xmm1 = swizzle(xmm0, .y, .x, .w, .x); // | y0*y1 | -- | w0*w1 | -- | + xmm1 = xmm0 + xmm1; // | x0*x1 + y0*y1 | -- | z0*z1 + w0*w1 | -- | + xmm0 = swizzle(xmm1, .z, .x, .x, .x); // | z0*z1 + w0*w1 | -- | -- | -- | + xmm0 = f32x4(xmm0[0] + xmm1[0], xmm0[1], xmm0[2], xmm0[2]); // addss + return swizzle(xmm0, .x, .x, .x, .x); +} + +inline fn lengthSq2(v: Vec) F32x4 { + return dot2(v, v); +} +inline fn lengthSq3(v: Vec) F32x4 { + return dot3(v, v); +} + +inline fn length2(v: Vec) F32x4 { + return @sqrt(dot2(v, v)); +} +inline fn length3(v: Vec) F32x4 { + return @sqrt(dot3(v, v)); +} +inline fn length4(v: Vec) F32x4 { + return @sqrt(dot4(v, v)); +} + +inline fn normalize2(v: Vec) Vec { + return v * splat(F32x4, 1.0) / @sqrt(dot2(v, v)); +} +inline fn normalize4(v: Vec) Vec { + return v * splat(F32x4, 1.0) / @sqrt(dot4(v, v)); +} + +fn vecMulMat(v: Vec, m: Mat) Vec { + const vx = @shuffle(f32, v, undefined, [4]i32{ 0, 0, 0, 0 }); + const vy = @shuffle(f32, v, undefined, [4]i32{ 1, 1, 1, 1 }); + const vz = @shuffle(f32, v, undefined, [4]i32{ 2, 2, 2, 2 }); + const vw = @shuffle(f32, v, undefined, [4]i32{ 3, 3, 3, 3 }); + return vx * m[0] + vy * m[1] + vz * m[2] + vw * m[3]; +} +fn matMulVec(m: Mat, v: Vec) Vec { + return .{ dot4(m[0], v)[0], dot4(m[1], v)[0], dot4(m[2], v)[0], dot4(m[3], v)[0] }; +} + +fn matFromArr(arr: [16]f32) Mat { + return Mat{ + f32x4(arr[0], arr[1], arr[2], arr[3]), + f32x4(arr[4], arr[5], arr[6], arr[7]), + f32x4(arr[8], arr[9], arr[10], arr[11]), + f32x4(arr[12], arr[13], arr[14], arr[15]), + }; +} + +fn mulRetType(comptime Ta: type, comptime Tb: type) type { + if (Ta == Mat and Tb == Mat) { + return Mat; + } else if ((Ta == f32 and Tb == Mat) or (Ta == Mat and Tb == f32)) { + return Mat; + } else if ((Ta == Vec and Tb == Mat) or (Ta == Mat and Tb == Vec)) { + return Vec; + } + @compileError("zmath.mul() not implemented for types: " ++ @typeName(Ta) ++ @typeName(Tb)); +} + +fn mulMat(m0: Mat, m1: Mat) Mat { + var result: Mat = undefined; + comptime var row: u32 = 0; + inline while (row < 4) : (row += 1) { + const vx = swizzle(m0[row], .x, .x, .x, .x); + const vy = swizzle(m0[row], .y, .y, .y, .y); + const vz = swizzle(m0[row], .z, .z, .z, .z); + const vw = swizzle(m0[row], .w, .w, .w, .w); + result[row] = mulAdd(vx, m1[0], vz * m1[2]) + mulAdd(vy, m1[1], vw * m1[3]); + } + return result; +} +fn lookToLh(eyepos: Vec, eyedir: Vec, updir: Vec) Mat { + const az = normalize3(eyedir); + const ax = normalize3(cross3(updir, az)); + const ay = normalize3(cross3(az, ax)); + return transpose(.{ + f32x4(ax[0], ax[1], ax[2], -dot3(ax, eyepos)[0]), + f32x4(ay[0], ay[1], ay[2], -dot3(ay, eyepos)[0]), + f32x4(az[0], az[1], az[2], -dot3(az, eyepos)[0]), + f32x4(0.0, 0.0, 0.0, 1.0), + }); +} + +fn inverseMat(m: Mat) Mat { + return inverseDet(m, null); +} + +fn inverseDet(m: Mat, out_det: ?*F32x4) Mat { + const mt = transpose(m); + var v0: [4]F32x4 = undefined; + var v1: [4]F32x4 = undefined; + + v0[0] = swizzle(mt[2], .x, .x, .y, .y); + v1[0] = swizzle(mt[3], .z, .w, .z, .w); + v0[1] = swizzle(mt[0], .x, .x, .y, .y); + v1[1] = swizzle(mt[1], .z, .w, .z, .w); + v0[2] = @shuffle(f32, mt[2], mt[0], [4]i32{ 0, 2, ~@as(i32, 0), ~@as(i32, 2) }); + v1[2] = @shuffle(f32, mt[3], mt[1], [4]i32{ 1, 3, ~@as(i32, 1), ~@as(i32, 3) }); + + var d0 = v0[0] * v1[0]; + var d1 = v0[1] * v1[1]; + var d2 = v0[2] * v1[2]; + + v0[0] = swizzle(mt[2], .z, .w, .z, .w); + v1[0] = swizzle(mt[3], .x, .x, .y, .y); + v0[1] = swizzle(mt[0], .z, .w, .z, .w); + v1[1] = swizzle(mt[1], .x, .x, .y, .y); + v0[2] = @shuffle(f32, mt[2], mt[0], [4]i32{ 1, 3, ~@as(i32, 1), ~@as(i32, 3) }); + v1[2] = @shuffle(f32, mt[3], mt[1], [4]i32{ 0, 2, ~@as(i32, 0), ~@as(i32, 2) }); + + d0 = mulAdd(-v0[0], v1[0], d0); + d1 = mulAdd(-v0[1], v1[1], d1); + d2 = mulAdd(-v0[2], v1[2], d2); + + v0[0] = swizzle(mt[1], .y, .z, .x, .y); + v1[0] = @shuffle(f32, d0, d2, [4]i32{ ~@as(i32, 1), 1, 3, 0 }); + v0[1] = swizzle(mt[0], .z, .x, .y, .x); + v1[1] = @shuffle(f32, d0, d2, [4]i32{ 3, ~@as(i32, 1), 1, 2 }); + v0[2] = swizzle(mt[3], .y, .z, .x, .y); + v1[2] = @shuffle(f32, d1, d2, [4]i32{ ~@as(i32, 3), 1, 3, 0 }); + v0[3] = swizzle(mt[2], .z, .x, .y, .x); + v1[3] = @shuffle(f32, d1, d2, [4]i32{ 3, ~@as(i32, 3), 1, 2 }); + + var c0 = v0[0] * v1[0]; + var c2 = v0[1] * v1[1]; + var c4 = v0[2] * v1[2]; + var c6 = v0[3] * v1[3]; + + v0[0] = swizzle(mt[1], .z, .w, .y, .z); + v1[0] = @shuffle(f32, d0, d2, [4]i32{ 3, 0, 1, ~@as(i32, 0) }); + v0[1] = swizzle(mt[0], .w, .z, .w, .y); + v1[1] = @shuffle(f32, d0, d2, [4]i32{ 2, 1, ~@as(i32, 0), 0 }); + v0[2] = swizzle(mt[3], .z, .w, .y, .z); + v1[2] = @shuffle(f32, d1, d2, [4]i32{ 3, 0, 1, ~@as(i32, 2) }); + v0[3] = swizzle(mt[2], .w, .z, .w, .y); + v1[3] = @shuffle(f32, d1, d2, [4]i32{ 2, 1, ~@as(i32, 2), 0 }); + + c0 = mulAdd(-v0[0], v1[0], c0); + c2 = mulAdd(-v0[1], v1[1], c2); + c4 = mulAdd(-v0[2], v1[2], c4); + c6 = mulAdd(-v0[3], v1[3], c6); + + v0[0] = swizzle(mt[1], .w, .x, .w, .x); + v1[0] = @shuffle(f32, d0, d2, [4]i32{ 2, ~@as(i32, 1), ~@as(i32, 0), 2 }); + v0[1] = swizzle(mt[0], .y, .w, .x, .z); + v1[1] = @shuffle(f32, d0, d2, [4]i32{ ~@as(i32, 1), 0, 3, ~@as(i32, 0) }); + v0[2] = swizzle(mt[3], .w, .x, .w, .x); + v1[2] = @shuffle(f32, d1, d2, [4]i32{ 2, ~@as(i32, 3), ~@as(i32, 2), 2 }); + v0[3] = swizzle(mt[2], .y, .w, .x, .z); + v1[3] = @shuffle(f32, d1, d2, [4]i32{ ~@as(i32, 3), 0, 3, ~@as(i32, 2) }); + + const c1 = mulAdd(-v0[0], v1[0], c0); + const c3 = mulAdd(v0[1], v1[1], c2); + const c5 = mulAdd(-v0[2], v1[2], c4); + const c7 = mulAdd(v0[3], v1[3], c6); + + c0 = mulAdd(v0[0], v1[0], c0); + c2 = mulAdd(-v0[1], v1[1], c2); + c4 = mulAdd(v0[2], v1[2], c4); + c6 = mulAdd(-v0[3], v1[3], c6); + + var mr = Mat{ + f32x4(c0[0], c1[1], c0[2], c1[3]), + f32x4(c2[0], c3[1], c2[2], c3[3]), + f32x4(c4[0], c5[1], c4[2], c5[3]), + f32x4(c6[0], c7[1], c6[2], c7[3]), + }; + + const det = dot4(mr[0], mt[0]); + if (out_det != null) { + out_det.?.* = det; + } + + if (math.approxEqAbs(f32, det[0], 0.0, math.floatEps(f32))) { + return .{ + f32x4(0.0, 0.0, 0.0, 0.0), + f32x4(0.0, 0.0, 0.0, 0.0), + f32x4(0.0, 0.0, 0.0, 0.0), + f32x4(0.0, 0.0, 0.0, 0.0), + }; + } + + const scale = splat(F32x4, 1.0) / det; + mr[0] *= scale; + mr[1] *= scale; + mr[2] *= scale; + mr[3] *= scale; + return mr; +} + +fn quatFromNormAxisAngle(axis: Vec, angle: f32) Quat { + const n = f32x4(axis[0], axis[1], axis[2], 1.0); + const sc = sincos(0.5 * angle); + return n * f32x4(sc[0], sc[0], sc[0], sc[1]); +} + +fn sin32(v: f32) f32 { + var y = v - math.tau * @round(v * 1.0 / math.tau); + + if (y > 0.5 * math.pi) { + y = math.pi - y; + } else if (y < -math.pi * 0.5) { + y = -math.pi - y; + } + const y2 = y * y; + + // 11-degree minimax approximation + var sinv = mulAdd(@as(f32, -2.3889859e-08), y2, 2.7525562e-06); + sinv = mulAdd(sinv, y2, -0.00019840874); + sinv = mulAdd(sinv, y2, 0.0083333310); + sinv = mulAdd(sinv, y2, -0.16666667); + return y * mulAdd(sinv, y2, 1.0); +} +fn cos32(v: f32) f32 { + var y = v - math.tau * @round(v * 1.0 / math.tau); + + const sign = blk: { + if (y > 0.5 * math.pi) { + y = math.pi - y; + break :blk @as(f32, -1.0); + } else if (y < -math.pi * 0.5) { + y = -math.pi - y; + break :blk @as(f32, -1.0); + } else { + break :blk @as(f32, 1.0); + } + }; + const y2 = y * y; + + // 10-degree minimax approximation + var cosv = mulAdd(@as(f32, -2.6051615e-07), y2, 2.4760495e-05); + cosv = mulAdd(cosv, y2, -0.0013888378); + cosv = mulAdd(cosv, y2, 0.041666638); + cosv = mulAdd(cosv, y2, -0.5); + return sign * mulAdd(cosv, y2, 1.0); +} +fn sincos32(v: f32) [2]f32 { + var y = v - math.tau * @round(v * 1.0 / math.tau); + + const sign = blk: { + if (y > 0.5 * math.pi) { + y = math.pi - y; + break :blk @as(f32, -1.0); + } else if (y < -math.pi * 0.5) { + y = -math.pi - y; + break :blk @as(f32, -1.0); + } else { + break :blk @as(f32, 1.0); + } + }; + const y2 = y * y; + + // 11-degree minimax approximation + var sinv = mulAdd(@as(f32, -2.3889859e-08), y2, 2.7525562e-06); + sinv = mulAdd(sinv, y2, -0.00019840874); + sinv = mulAdd(sinv, y2, 0.0083333310); + sinv = mulAdd(sinv, y2, -0.16666667); + sinv = y * mulAdd(sinv, y2, 1.0); + + // 10-degree minimax approximation + var cosv = mulAdd(@as(f32, -2.6051615e-07), y2, 2.4760495e-05); + cosv = mulAdd(cosv, y2, -0.0013888378); + cosv = mulAdd(cosv, y2, 0.041666638); + cosv = mulAdd(cosv, y2, -0.5); + cosv = sign * mulAdd(cosv, y2, 1.0); + + return .{ sinv, cosv }; +} + +fn modAngle32(in_angle: f32) f32 { + const angle = in_angle + math.pi; + var temp: f32 = @abs(angle); + temp = temp - (2.0 * math.pi * @as(f32, @floatFromInt(@as(i32, @intFromFloat(temp / math.pi))))); + temp = temp - math.pi; + if (angle < 0.0) { + temp = -temp; + } + return temp; +} + +const f32x4_sign_mask1: F32x4 = F32x4{ @as(f32, @bitCast(@as(u32, 0x8000_0000))), 0, 0, 0 }; +const f32x4_mask2: F32x4 = F32x4{ + @as(f32, @bitCast(@as(u32, 0xffff_ffff))), + @as(f32, @bitCast(@as(u32, 0xffff_ffff))), + 0, + 0, +}; +const f32x4_mask3: F32x4 = F32x4{ + @as(f32, @bitCast(@as(u32, 0xffff_ffff))), + @as(f32, @bitCast(@as(u32, 0xffff_ffff))), + @as(f32, @bitCast(@as(u32, 0xffff_ffff))), + 0, +}; + +inline fn splatNegativeZero(comptime T: type) T { + return @splat(@as(f32, @bitCast(@as(u32, 0x8000_0000)))); +} +inline fn splatNoFraction(comptime T: type) T { + return @splat(@as(f32, 8_388_608.0)); +} + +fn floatToIntAndBack(v: anytype) @TypeOf(v) { + // This routine won't handle nan, inf and numbers greater than 8_388_608.0 (will generate undefined values). + @setRuntimeSafety(false); + + const T = @TypeOf(v); + const len = veclen(T); + + var vi32: [len]i32 = undefined; + comptime var i: u32 = 0; + // vcvttps2dq + inline while (i < len) : (i += 1) { + vi32[i] = @as(i32, @intFromFloat(v[i])); + } + + var vf32: [len]f32 = undefined; + i = 0; + // vcvtdq2ps + inline while (i < len) : (i += 1) { + vf32[i] = @as(f32, @floatFromInt(vi32[i])); + } + + return vf32; +} + +fn approxEqAbs(v0: anytype, v1: anytype, eps: f32) bool { + const T = @TypeOf(v0, v1); + comptime var i: comptime_int = 0; + inline while (i < veclen(T)) : (i += 1) { + if (!math.approxEqAbs(f32, v0[i], v1[i], eps)) { + return false; + } + } + return true; +} diff --git a/src/core/main.zig b/src/core/main.zig new file mode 100644 index 00000000..043f1a6b --- /dev/null +++ b/src/core/main.zig @@ -0,0 +1,761 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +pub const sysgpu = @import("../main.zig").sysgpu; +pub const sysjs = @import("mach-sysjs"); +pub const Timer = @import("Timer.zig"); +const Frequency = @import("Frequency.zig"); +const platform = @import("platform.zig"); + +/// Returns the error set that the function F returns. +fn ErrorSet(comptime F: type) type { + return @typeInfo(@typeInfo(F).Fn.return_type.?).ErrorUnion.error_set; +} + +/// Comptime options that you can configure in your main file by writing e.g.: +/// +/// ``` +/// pub const mach_core_options = core.ComptimeOptions{ +/// .use_wgpu = true, +/// .use_sysgpu = true, +/// }; +/// ``` +pub const ComptimeOptions = struct { + /// Whether to use + use_wgpu: bool = true, + + /// Whether or not to use the experimental sysgpu graphics API. + use_sysgpu: bool = false, +}; + +pub const options = if (@hasDecl(@import("root"), "mach_core_options")) + @import("root").mach_core_options +else + ComptimeOptions{}; + +pub const wgpu = @import("mach-gpu"); + +pub const gpu = if (options.use_sysgpu) sysgpu.sysgpu else wgpu; + +pub fn AppInterface(comptime app_entry: anytype) void { + if (!@hasDecl(app_entry, "App")) { + @compileError("expected e.g. `pub const App = mach.App(modules, init)' (App definition missing in your main Zig file)"); + } + + const App = app_entry.App; + if (@typeInfo(App) != .Struct) { + @compileError("App must be a struct type. Found:" ++ @typeName(App)); + } + + if (@hasDecl(App, "init")) { + const InitFn = @TypeOf(@field(App, "init")); + if (InitFn != fn (app: *App) ErrorSet(InitFn)!void) + @compileError("expected 'pub fn init(app: *App) !void' found '" ++ @typeName(InitFn) ++ "'"); + } else { + @compileError("App must export 'pub fn init(app: *App) !void'"); + } + + if (@hasDecl(App, "update")) { + const UpdateFn = @TypeOf(@field(App, "update")); + if (UpdateFn != fn (app: *App) ErrorSet(UpdateFn)!bool) + @compileError("expected 'pub fn update(app: *App) !bool' found '" ++ @typeName(UpdateFn) ++ "'"); + } else { + @compileError("App must export 'pub fn update(app: *App) !bool'"); + } + + if (@hasDecl(App, "updateMainThread")) { + const UpdateMainThreadFn = @TypeOf(@field(App, "updateMainThread")); + if (UpdateMainThreadFn != fn (app: *App) ErrorSet(UpdateMainThreadFn)!bool) + @compileError("expected 'pub fn updateMainThread(app: *App) !bool' found '" ++ @typeName(UpdateMainThreadFn) ++ "'"); + } + + if (@hasDecl(App, "deinit")) { + const DeinitFn = @TypeOf(@field(App, "deinit")); + if (DeinitFn != fn (app: *App) void) + @compileError("expected 'pub fn deinit(app: *App) void' found '" ++ @typeName(DeinitFn) ++ "'"); + } else { + @compileError("App must export 'pub fn deinit(app: *App) void'"); + } +} + +/// wasm32: custom std.log implementation which logs to the browser console. +/// other: std.log.defaultLog +pub const defaultLog = platform.Core.defaultLog; + +/// wasm32: custom @panic implementation which logs to the browser console. +/// other: std.debug.default_panic +pub const defaultPanic = platform.Core.defaultPanic; + +/// The allocator used by mach-core for any allocations. Must be specified before the first call to +/// core.init() +pub var allocator: std.mem.Allocator = undefined; + +/// A buffer which you may use to write the window title to. See core.setTitle() for details. +pub var title: [256:0]u8 = undefined; + +/// May be read inside `App.init`, `App.update`, and `App.deinit`. +/// +/// No synchronization is performed, so these fields may not be accessed in `App.updateMainThread`. +pub var adapter: *gpu.Adapter = undefined; +pub var device: *gpu.Device = undefined; +pub var queue: *gpu.Queue = undefined; +pub var swap_chain: *gpu.SwapChain = undefined; +pub var descriptor: gpu.SwapChain.Descriptor = undefined; + +/// The time in seconds between the last frame and the current frame. +/// +/// Higher frame rates will report higher values, for example if your application is running at +/// 60FPS this will report 0.01666666666 (1.0 / 60) seconds, and if it is running at 30FPS it will +/// report twice that, 0.03333333333 (1.0 / 30.0) seconds. +/// +/// For example, instead of rotating an object 360 degrees every frame `rotation += 6.0` (one full +/// rotation every second, but only if your application is running at 60FPS) you may instead multiply +/// by this number `rotation += 360.0 * core.delta_time` which results in one full rotation every +/// second, no matter what frame rate the application is running at. +pub var delta_time: f32 = 0; +pub var delta_time_ns: u64 = 0; + +var frame: Frequency = undefined; +var input: Frequency = undefined; +var internal: platform.Core = undefined; + +/// All memory will be copied or returned to the caller once init() finishes. +pub const Options = struct { + is_app: bool = false, + headless: bool = false, + display_mode: DisplayMode = .windowed, + border: bool = true, + title: [:0]const u8 = "Mach core", + size: Size = .{ .width = 1920 / 2, .height = 1080 / 2 }, + power_preference: gpu.PowerPreference = .undefined, + required_features: ?[]const gpu.FeatureName = null, + required_limits: ?gpu.Limits = null, +}; + +pub fn init(options_in: Options) !void { + // Copy window title into owned buffer. + var opt = options_in; + if (opt.title.len < title.len) { + @memcpy(title[0..opt.title.len], opt.title); + title[opt.title.len] = 0; + opt.title = title[0..opt.title.len :0]; + } + + frame = .{ + .target = 0, + .delta_time = &delta_time, + .delta_time_ns = &delta_time_ns, + }; + input = .{ .target = 1 }; + + try platform.Core.init( + &internal, + allocator, + &frame, + &input, + opt, + ); +} + +pub inline fn deinit() void { + return internal.deinit(); +} + +pub inline fn update(app_ptr: anytype) !bool { + return try internal.update(app_ptr); +} + +pub const EventIterator = struct { + internal: platform.Core.EventIterator, + + pub inline fn next(self: *EventIterator) ?Event { + return self.internal.next(); + } +}; + +pub inline fn pollEvents() EventIterator { + return .{ .internal = internal.pollEvents() }; +} + +/// Sets the window title. The string must be owned by Core, and will not be copied or freed. It is +/// advised to use the `core.title` buffer for this purpose, e.g.: +/// +/// ``` +/// const title = try std.fmt.bufPrintZ(&core.title, "Hello, world!", .{}); +/// core.setTitle(title); +/// ``` +pub inline fn setTitle(value: [:0]const u8) void { + return internal.setTitle(value); +} + +/// Sets the window title. Uses the `core.title` buffer. +pub inline fn printTitle(fmt: []const u8, args: anytype) !void { + const value = try std.fmt.bufPrintZ(&title, fmt, args); + return internal.setTitle(value); +} + +/// Set the window mode +pub inline fn setDisplayMode(mode: DisplayMode) void { + return internal.setDisplayMode(mode); +} + +/// Returns the window mode +pub inline fn displayMode() DisplayMode { + return internal.displayMode(); +} + +pub inline fn setBorder(value: bool) void { + return internal.setBorder(value); +} + +pub inline fn border() bool { + return internal.border(); +} + +pub inline fn setHeadless(value: bool) void { + return internal.setHeadless(value); +} + +pub inline fn headless() bool { + return internal.headless(); +} + +pub const VSyncMode = enum { + /// Potential screen tearing. + /// No synchronization with monitor, render frames as fast as possible. + /// + /// Not available on WASM, fallback to double + none, + + /// No tearing, synchronizes rendering with monitor refresh rate, rendering frames when ready. + /// + /// Tries to stay one frame ahead of the monitor, so when it's ready for the next frame it is + /// already prepared. + double, + + /// No tearing, synchronizes rendering with monitor refresh rate, rendering frames when ready. + /// + /// Tries to stay two frames ahead of the monitor, so when it's ready for the next frame it is + /// already prepared. + /// + /// Not available on WASM, fallback to double + triple, +}; + +/// Set refresh rate synchronization mode. Default `.triple` +/// +/// Calling this function also implicitly calls setFrameRateLimit for you: +/// ``` +/// .none => setFrameRateLimit(0) // unlimited +/// .double => setFrameRateLimit(0) // unlimited +/// .triple => setFrameRateLimit(2 * max_monitor_refresh_rate) +/// ``` +pub inline fn setVSync(mode: VSyncMode) void { + return internal.setVSync(mode); +} + +/// Returns refresh rate synchronization mode. +pub inline fn vsync() VSyncMode { + return internal.vsync(); +} + +/// Sets the frame rate limit. Default 0 (unlimited) +/// +/// This is applied *in addition* to the vsync mode. +pub inline fn setFrameRateLimit(limit: u32) void { + frame.target = limit; +} + +/// Returns the frame rate limit, or zero if unlimited. +pub inline fn frameRateLimit() u32 { + return frame.target; +} + +/// Set the window size, in subpixel units. +pub inline fn setSize(value: Size) void { + return internal.setSize(value); +} + +/// Returns the window size, in subpixel units. +pub inline fn size() Size { + return internal.size(); +} + +/// Set the minimum and maximum allowed size for the window. +pub inline fn setSizeLimit(size_limit: SizeLimit) void { + return internal.setSizeLimit(size_limit); +} + +/// Returns the minimum and maximum allowed size for the window. +pub inline fn sizeLimit() SizeLimit { + return internal.sizeLimit(); +} + +pub inline fn setCursorMode(mode: CursorMode) void { + return internal.setCursorMode(mode); +} + +pub inline fn cursorMode() CursorMode { + return internal.cursorMode(); +} + +pub inline fn setCursorShape(cursor: CursorShape) void { + return internal.setCursorShape(cursor); +} + +pub inline fn cursorShape() CursorShape { + return internal.cursorShape(); +} + +// TODO(feature): add joystick/gamepad support https://github.com/hexops/mach/issues/884 + +// /// Checks if the given joystick is still connected. +// pub inline fn joystickPresent(joystick: Joystick) bool { +// return internal.joystickPresent(joystick); +// } + +// /// Retreives the name of the joystick. +// /// Returns `null` if the joystick isnt connected. +// pub inline fn joystickName(joystick: Joystick) ?[:0]const u8 { +// return internal.joystickName(joystick); +// } + +// /// Retrieves the state of the buttons of the given joystick. +// /// A value of `true` indicates the button is pressed, `false` the button is released. +// /// No remapping is done, so the order of these buttons are joystick-dependent and should be +// /// consistent across platforms. +// /// +// /// Returns `null` if the joystick isnt connected. +// /// +// /// Note: For WebAssembly, the remapping is done directly by the web browser, so on that platform +// /// the order of these buttons might be different than on others. +// pub inline fn joystickButtons(joystick: Joystick) ?[]const bool { +// return internal.joystickButtons(joystick); +// } + +// /// Retreives the state of the axes of the given joystick. +// /// The values are always from -1 to 1. +// /// No remapping is done, so the order of these axes are joytstick-dependent and should be +// /// consistent acrsoss platforms. +// /// +// /// Returns `null` if the joystick isnt connected. +// /// +// /// Note: For WebAssembly, the remapping is done directly by the web browser, so on that platform +// /// the order of these axes might be different than on others. +// pub inline fn joystickAxes(joystick: Joystick) ?[]const f32 { +// return internal.joystickAxes(joystick); +// } + +pub inline fn keyPressed(key: Key) bool { + return internal.keyPressed(key); +} + +pub inline fn keyReleased(key: Key) bool { + return internal.keyReleased(key); +} + +pub inline fn mousePressed(button: MouseButton) bool { + return internal.mousePressed(button); +} + +pub inline fn mouseReleased(button: MouseButton) bool { + return internal.mouseReleased(button); +} + +pub inline fn mousePosition() Position { + return internal.mousePosition(); +} + +/// Whether mach core has run out of memory. If true, freeing memory should restore it to a +/// functional state. +/// +/// Once called, future calls will return false until another OOM error occurs. +/// +/// Note that if an App.update function returns any error, including errors.OutOfMemory, it will +/// exit the application. +pub inline fn outOfMemory() bool { + return internal.outOfMemory(); +} + +/// Asks to wake the main thread. This e.g. allows your `pub fn update` to ask that the main thread +/// transition away from waiting for input, and execute another cycle which involves calling the +/// optional `updateMainThread` callback. +/// +/// For example, instead of increasing the input thread target frequency, you may just call this +/// function to wake the main thread when your `updateMainThread` callback needs to be ran. +/// +/// May be called from any thread. +pub inline fn wakeMainThread() void { + internal.wakeMainThread(); +} + +/// Sets the minimum target frequency of the input handling thread. +/// +/// Input handling (the main thread) runs at a variable frequency. The thread blocks until there are +/// input events available, or until it needs to unblock in order to achieve the minimum target +/// frequency which is your collaboration point of opportunity with the main thread. +/// +/// For example, by default (`setInputFrequency(1)`) mach-core will aim to invoke `updateMainThread` +/// at least once per second (but potentially much more, e.g. once per every mouse movement or +/// keyboard button press.) If you were to increase the input frequency to say 60hz e.g. +/// `setInputFrequency(60)` then mach-core will aim to invoke your `updateMainThread` 60 times per +/// second. +/// +/// An input frequency of zero implies unlimited, in which case the main thread will busy-wait. +/// +/// # Multithreaded mach-core behavior +/// +/// On some platforms, mach-core is able to handle input and rendering independently for +/// improved performance and responsiveness. +/// +/// | Platform | Threading | +/// |----------|-----------------| +/// | Desktop | Multi threaded | +/// | Browser | Single threaded | +/// | Mobile | TBD | +/// +/// On single-threaded platforms, `update` and the (optional) `updateMainThread` callback are +/// invoked in sequence, one after the other, on the same thread. +/// +/// On multi-threaded platforms, `init` and `deinit` are called on the main thread, while `update` +/// is called on a separate rendering thread. The (optional) `updateMainThread` callback can be +/// used in cases where you must run a function on the main OS thread (such as to open a native +/// file dialog on macOS, since many system GUI APIs must be run on the main OS thread.) It is +/// advised you do not use this callback to run any code except when absolutely neccessary, as +/// it is in direct contention with input handling. +/// +/// APIs which are not accessible from a specific thread are declared as such, otherwise can be +/// called from any thread as they are internally synchronized. +pub inline fn setInputFrequency(input_frequency: u32) void { + input.target = input_frequency; +} + +/// Returns the input frequency, or zero if unlimited (busy-waiting mode) +pub inline fn inputFrequency() u32 { + return input.target; +} + +/// Returns the actual number of frames rendered (`update` calls that returned) in the last second. +/// +/// This is updated once per second. +pub inline fn frameRate() u32 { + return frame.rate; +} + +/// Returns the actual number of input thread iterations in the last second. See setInputFrequency +/// for what this means. +/// +/// This is updated once per second. +pub inline fn inputRate() u32 { + return input.rate; +} + +/// Returns the underlying native NSWindow pointer +/// +/// May only be called on macOS. +pub fn nativeWindowCocoa() *anyopaque { + return internal.nativeWindowCocoa(); +} + +/// Returns the underlying native Windows' HWND pointer +/// +/// May only be called on Windows. +pub fn nativeWindowWin32() std.os.windows.HWND { + return internal.nativeWindowWin32(); +} + +pub const Size = struct { + width: u32, + height: u32, + + pub inline fn eql(a: Size, b: Size) bool { + return a.width == b.width and a.height == b.height; + } +}; + +pub const SizeOptional = struct { + width: ?u32 = null, + height: ?u32 = null, + + pub inline fn eql(a: SizeOptional, b: SizeOptional) bool { + if ((a.width != null) != (b.width != null)) return false; + if ((a.height != null) != (b.height != null)) return false; + + if (a.width != null and a.width.? != b.width.?) return false; + if (a.height != null and a.height.? != b.height.?) return false; + return true; + } +}; + +pub const SizeLimit = struct { + min: SizeOptional, + max: SizeOptional, + + pub inline fn eql(a: SizeLimit, b: SizeLimit) bool { + return a.min.eql(b.min) and a.max.eql(b.max); + } +}; + +pub const Position = struct { + x: f64, + y: f64, +}; + +pub const Event = union(enum) { + key_press: KeyEvent, + key_repeat: KeyEvent, + key_release: KeyEvent, + char_input: struct { + codepoint: u21, + }, + mouse_motion: struct { + pos: Position, + }, + mouse_press: MouseButtonEvent, + mouse_release: MouseButtonEvent, + mouse_scroll: struct { + xoffset: f32, + yoffset: f32, + }, + joystick_connected: Joystick, + joystick_disconnected: Joystick, + framebuffer_resize: Size, + focus_gained, + focus_lost, + close, +}; + +pub const KeyEvent = struct { + key: Key, + mods: KeyMods, +}; + +pub const MouseButtonEvent = struct { + button: MouseButton, + pos: Position, + mods: KeyMods, +}; + +pub const MouseButton = enum { + left, + right, + middle, + four, + five, + six, + seven, + eight, + + pub const max = MouseButton.eight; +}; + +pub const Key = enum { + a, + b, + c, + d, + e, + f, + g, + h, + i, + j, + k, + l, + m, + n, + o, + p, + q, + r, + s, + t, + u, + v, + w, + x, + y, + z, + + zero, + one, + two, + three, + four, + five, + six, + seven, + eight, + nine, + + f1, + f2, + f3, + f4, + f5, + f6, + f7, + f8, + f9, + f10, + f11, + f12, + f13, + f14, + f15, + f16, + f17, + f18, + f19, + f20, + f21, + f22, + f23, + f24, + f25, + + kp_divide, + kp_multiply, + kp_subtract, + kp_add, + kp_0, + kp_1, + kp_2, + kp_3, + kp_4, + kp_5, + kp_6, + kp_7, + kp_8, + kp_9, + kp_decimal, + kp_equal, + kp_enter, + + enter, + escape, + tab, + left_shift, + right_shift, + left_control, + right_control, + left_alt, + right_alt, + left_super, + right_super, + menu, + num_lock, + caps_lock, + print, + scroll_lock, + pause, + delete, + home, + end, + page_up, + page_down, + insert, + left, + right, + up, + down, + backspace, + space, + minus, + equal, + left_bracket, + right_bracket, + backslash, + semicolon, + apostrophe, + comma, + period, + slash, + grave, + + unknown, + + pub const max = Key.unknown; +}; + +pub const KeyMods = packed struct(u8) { + shift: bool, + control: bool, + alt: bool, + super: bool, + caps_lock: bool, + num_lock: bool, + _padding: u2 = 0, +}; + +pub const DisplayMode = enum { + /// Windowed mode. + windowed, + + /// Fullscreen mode, using this option may change the display's video mode. + fullscreen, + + /// Borderless fullscreen window. + /// + /// Beware that true .fullscreen is also a hint to the OS that is used in various contexts, e.g. + /// + /// * macOS: Moving to a virtual space dedicated to fullscreen windows as the user expects + /// * macOS: .borderless windows cannot prevent the system menu bar from being displayed + /// + /// Always allow users to choose their preferred display mode. + borderless, +}; + +pub const CursorMode = enum { + /// Makes the cursor visible and behaving normally. + normal, + + /// Makes the cursor invisible when it is over the content area of the window but does not + /// restrict it from leaving. + hidden, + + /// Hides and grabs the cursor, providing virtual and unlimited cursor movement. This is useful + /// for implementing for example 3D camera controls. + disabled, +}; + +pub const CursorShape = enum { + arrow, + ibeam, + crosshair, + pointing_hand, + resize_ew, + resize_ns, + resize_nwse, + resize_nesw, + resize_all, + not_allowed, +}; + +pub const Joystick = enum(u8) { + zero, +}; + +test { + @import("std").testing.refAllDecls(Timer); + @import("std").testing.refAllDecls(Frequency); + @import("std").testing.refAllDecls(platform); + + @import("std").testing.refAllDeclsRecursive(Options); + @import("std").testing.refAllDeclsRecursive(EventIterator); + @import("std").testing.refAllDeclsRecursive(VSyncMode); + @import("std").testing.refAllDeclsRecursive(Size); + @import("std").testing.refAllDeclsRecursive(SizeOptional); + @import("std").testing.refAllDeclsRecursive(SizeLimit); + @import("std").testing.refAllDeclsRecursive(Position); + @import("std").testing.refAllDeclsRecursive(Event); + @import("std").testing.refAllDeclsRecursive(KeyEvent); + @import("std").testing.refAllDeclsRecursive(MouseButtonEvent); + @import("std").testing.refAllDeclsRecursive(MouseButton); + @import("std").testing.refAllDeclsRecursive(Key); + @import("std").testing.refAllDeclsRecursive(KeyMods); + @import("std").testing.refAllDeclsRecursive(DisplayMode); + @import("std").testing.refAllDeclsRecursive(CursorMode); + @import("std").testing.refAllDeclsRecursive(CursorShape); + @import("std").testing.refAllDeclsRecursive(Joystick); +} diff --git a/src/core/platform.zig b/src/core/platform.zig new file mode 100644 index 00000000..1768f94f --- /dev/null +++ b/src/core/platform.zig @@ -0,0 +1,74 @@ +const builtin = @import("builtin"); +const options = @import("build-options"); + +const use_glfw = true; +const use_x11 = false; +const platform = switch (options.core_platform) { + .glfw => @import("platform/glfw.zig"), + .x11 => @import("platform/x11.zig"), + .wayland => @import("platform/wayland.zig"), + .web => @import("platform/wasm.zig"), +}; + +pub const Core = platform.Core; +pub const Timer = platform.Timer; + +// Verifies that a platform implementation exposes the expected function declarations. +comptime { + assertHasDecl(@This(), "Core"); + assertHasDecl(@This(), "Timer"); + + // Core + assertHasDecl(@This().Core, "init"); + assertHasDecl(@This().Core, "deinit"); + assertHasDecl(@This().Core, "pollEvents"); + + assertHasDecl(@This().Core, "setTitle"); + + assertHasDecl(@This().Core, "setDisplayMode"); + assertHasDecl(@This().Core, "displayMode"); + + assertHasDecl(@This().Core, "setBorder"); + assertHasDecl(@This().Core, "border"); + + assertHasDecl(@This().Core, "setHeadless"); + assertHasDecl(@This().Core, "headless"); + + assertHasDecl(@This().Core, "setVSync"); + assertHasDecl(@This().Core, "vsync"); + + assertHasDecl(@This().Core, "setSize"); + assertHasDecl(@This().Core, "size"); + + assertHasDecl(@This().Core, "setSizeLimit"); + assertHasDecl(@This().Core, "sizeLimit"); + + assertHasDecl(@This().Core, "setCursorMode"); + assertHasDecl(@This().Core, "cursorMode"); + + assertHasDecl(@This().Core, "setCursorShape"); + assertHasDecl(@This().Core, "cursorShape"); + + assertHasDecl(@This().Core, "joystickPresent"); + assertHasDecl(@This().Core, "joystickName"); + assertHasDecl(@This().Core, "joystickButtons"); + assertHasDecl(@This().Core, "joystickAxes"); + + assertHasDecl(@This().Core, "keyPressed"); + assertHasDecl(@This().Core, "keyReleased"); + assertHasDecl(@This().Core, "mousePressed"); + assertHasDecl(@This().Core, "mouseReleased"); + assertHasDecl(@This().Core, "mousePosition"); + + assertHasDecl(@This().Core, "outOfMemory"); + + // Timer + assertHasDecl(@This().Timer, "start"); + assertHasDecl(@This().Timer, "read"); + assertHasDecl(@This().Timer, "reset"); + assertHasDecl(@This().Timer, "lap"); +} + +fn assertHasDecl(comptime T: anytype, comptime name: []const u8) void { + if (!@hasDecl(T, name)) @compileError("Core missing declaration: " ++ name); +} diff --git a/src/core/platform/common.zig b/src/core/platform/common.zig new file mode 100644 index 00000000..2cc77d91 --- /dev/null +++ b/src/core/platform/common.zig @@ -0,0 +1,89 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const mach = @import("../../main.zig"); +const gamemode = mach.gamemode; +const mach_core = mach.core; +const gpu = mach_core.gpu; + +pub inline fn printUnhandledErrorCallback(_: void, ty: gpu.ErrorType, message: [*:0]const u8) void { + switch (ty) { + .validation => std.log.err("gpu: validation error: {s}\n", .{message}), + .out_of_memory => std.log.err("gpu: out of memory: {s}\n", .{message}), + .device_lost => std.log.err("gpu: device lost: {s}\n", .{message}), + .unknown => std.log.err("gpu: unknown error: {s}\n", .{message}), + else => unreachable, + } + std.os.exit(1); +} + +pub fn detectBackendType(allocator: std.mem.Allocator) !gpu.BackendType { + const backend = std.process.getEnvVarOwned( + allocator, + "MACH_GPU_BACKEND", + ) catch |err| switch (err) { + error.EnvironmentVariableNotFound => { + if (builtin.target.isDarwin()) return .metal; + if (builtin.target.os.tag == .windows) return .d3d12; + return .vulkan; + }, + else => return err, + }; + defer allocator.free(backend); + + if (std.ascii.eqlIgnoreCase(backend, "null")) return .null; + if (std.ascii.eqlIgnoreCase(backend, "d3d11")) return .d3d11; + if (std.ascii.eqlIgnoreCase(backend, "d3d12")) return .d3d12; + if (std.ascii.eqlIgnoreCase(backend, "metal")) return .metal; + if (std.ascii.eqlIgnoreCase(backend, "vulkan")) return .vulkan; + if (std.ascii.eqlIgnoreCase(backend, "opengl")) return .opengl; + if (std.ascii.eqlIgnoreCase(backend, "opengles")) return .opengles; + + @panic("unknown MACH_GPU_BACKEND type"); +} + +/// Check if gamemode should be activated +pub fn wantGamemode(allocator: std.mem.Allocator) error{ OutOfMemory, InvalidUtf8 }!bool { + const use_gamemode = std.process.getEnvVarOwned( + allocator, + "MACH_USE_GAMEMODE", + ) catch |err| switch (err) { + error.EnvironmentVariableNotFound => return true, + else => |e| return e, + }; + defer allocator.free(use_gamemode); + + return !(std.ascii.eqlIgnoreCase(use_gamemode, "off") or std.ascii.eqlIgnoreCase(use_gamemode, "false")); +} + +const gamemode_log = std.log.scoped(.gamemode); + +pub fn initLinuxGamemode() bool { + gamemode.start(); + if (!gamemode.isActive()) return false; + gamemode_log.info("gamemode: activated", .{}); + return true; +} + +pub fn deinitLinuxGamemode() void { + gamemode.stop(); + gamemode_log.info("gamemode: deactivated", .{}); +} + +pub const RequestAdapterResponse = struct { + status: gpu.RequestAdapterStatus, + adapter: ?*gpu.Adapter, + message: ?[*:0]const u8, +}; + +pub inline fn requestAdapterCallback( + context: *RequestAdapterResponse, + status: gpu.RequestAdapterStatus, + adapter: ?*gpu.Adapter, + message: ?[*:0]const u8, +) void { + context.* = RequestAdapterResponse{ + .status = status, + .adapter = adapter, + .message = message, + }; +} diff --git a/src/core/platform/glfw.zig b/src/core/platform/glfw.zig new file mode 100644 index 00000000..32f1d056 --- /dev/null +++ b/src/core/platform/glfw.zig @@ -0,0 +1,4 @@ +const std = @import("std"); + +pub const Core = @import("glfw/Core.zig"); +pub const Timer = std.time.Timer; diff --git a/src/core/platform/glfw/Core.zig b/src/core/platform/glfw/Core.zig new file mode 100644 index 00000000..ae58d5f9 --- /dev/null +++ b/src/core/platform/glfw/Core.zig @@ -0,0 +1,1363 @@ +const builtin = @import("builtin"); +const std = @import("std"); +const glfw = @import("mach-glfw"); +const mach_core = @import("../../main.zig"); +const gpu = mach_core.gpu; +const objc = @import("objc.zig"); +const Options = @import("../../main.zig").Options; +const Event = @import("../../main.zig").Event; +const KeyEvent = @import("../../main.zig").KeyEvent; +const MouseButtonEvent = @import("../../main.zig").MouseButtonEvent; +const MouseButton = @import("../../main.zig").MouseButton; +const Size = @import("../../main.zig").Size; +const DisplayMode = @import("../../main.zig").DisplayMode; +const SizeLimit = @import("../../main.zig").SizeLimit; +const CursorShape = @import("../../main.zig").CursorShape; +const VSyncMode = @import("../../main.zig").VSyncMode; +const CursorMode = @import("../../main.zig").CursorMode; +const Key = @import("../../main.zig").Key; +const KeyMods = @import("../../main.zig").KeyMods; +const Joystick = @import("../../main.zig").Joystick; +const InputState = @import("../../InputState.zig"); +const Frequency = @import("../../Frequency.zig"); + +const log = std.log.scoped(.mach); + +pub const defaultLog = std.log.defaultLog; +pub const defaultPanic = std.debug.panicImpl; + +pub const Core = @This(); + +// needed for the glfw joystick callback +var core_instance: ?*Core = null; + +// There are two threads: +// +// 1. Main thread (App.init, App.deinit) which may interact with GLFW and handles events +// 2. App.update thread. + +// Read-only fields +allocator: std.mem.Allocator, +frame: *Frequency, +input: *Frequency, +window: glfw.Window, +backend_type: gpu.BackendType, +user_ptr: UserPtr, +instance: *gpu.Instance, +surface: *gpu.Surface, +gpu_adapter: *gpu.Adapter, +gpu_device: *gpu.Device, +max_refresh_rate: u32, + +// Mutable fields only used by main thread +app_update_thread_started: bool = false, +linux_gamemode: ?bool = null, +cursors: [@typeInfo(CursorShape).Enum.fields.len]?glfw.Cursor, +cursors_tried: [@typeInfo(CursorShape).Enum.fields.len]bool, +last_windowed_size: glfw.Window.Size, +last_windowed_pos: glfw.Window.Pos, + +// Event queue; written from main thread; read from any +events_mu: std.Thread.RwLock = .{}, +events: EventQueue, + +// Input state; written from main thread; read from any +input_mu: std.Thread.RwLock = .{}, +input_state: InputState = .{}, +present_joysticks: std.StaticBitSet(@typeInfo(glfw.Joystick.Id).Enum.fields.len), + +// Signals to the App.update thread to do something +swap_chain_update: std.Thread.ResetEvent = .{}, +state_update: std.Thread.ResetEvent = .{}, +done: std.Thread.ResetEvent = .{}, +oom: std.Thread.ResetEvent = .{}, + +// Mutable fields; written by the App.update thread, read from any +swap_chain_mu: std.Thread.RwLock = .{}, +swap_chain_desc: gpu.SwapChain.Descriptor, +swap_chain: *gpu.SwapChain, + +// Mutable state fields; read/write by any thread +state_mu: std.Thread.Mutex = .{}, +current_title: [:0]const u8, +current_title_changed: bool = false, +current_display_mode: DisplayMode = .windowed, +current_vsync_mode: VSyncMode = .triple, +last_display_mode: DisplayMode = .windowed, +last_vsync_mode: VSyncMode = .triple, +current_border: bool, +last_border: bool, +current_headless: bool, +last_headless: bool, +current_size: Size, +last_size: Size, +current_size_limit: SizeLimit = .{ + .min = .{ .width = 350, .height = 350 }, + .max = .{ .width = null, .height = null }, +}, +last_size_limit: SizeLimit = .{ .min = .{}, .max = .{} }, +current_cursor_mode: CursorMode = .normal, +last_cursor_mode: CursorMode = .normal, +current_cursor_shape: CursorShape = .arrow, +last_cursor_shape: CursorShape = .arrow, + +const EventQueue = std.fifo.LinearFifo(Event, .Dynamic); + +pub const EventIterator = struct { + events_mu: *std.Thread.RwLock, + queue: *EventQueue, + + pub inline fn next(self: *EventIterator) ?Event { + self.events_mu.lockShared(); + defer self.events_mu.unlockShared(); + return self.queue.readItem(); + } +}; + +const UserPtr = struct { + self: *Core, +}; + +// TODO(important): expose device loss to users, this can happen especially in the web and on mobile +// devices. Users will need to re-upload all assets to the GPU in this event. +fn deviceLostCallback(reason: gpu.Device.LostReason, msg: [*:0]const u8, userdata: ?*anyopaque) callconv(.C) void { + _ = userdata; + _ = reason; + log.err("mach: device lost: {s}", .{msg}); + @panic("mach: device lost"); +} + +// Called on the main thread +pub fn init( + core: *Core, + allocator: std.mem.Allocator, + frame: *Frequency, + input: *Frequency, + options: Options, +) !void { + if (!@import("builtin").is_test and mach_core.options.use_wgpu) _ = mach_core.wgpu.Export(@import("root").GPUInterface); + if (!@import("builtin").is_test and mach_core.options.use_sysgpu) _ = mach_core.sysgpu.sysgpu.Export(@import("root").SYSGPUInterface); + + const backend_type = try detectBackendType(allocator); + + glfw.setErrorCallback(errorCallback); + if (!glfw.init(.{})) { + glfw.getErrorCode() catch |err| switch (err) { + error.PlatformError, + error.PlatformUnavailable, + => return err, + else => unreachable, + }; + } + + // Create the test window and discover adapters using it (esp. for OpenGL) + var hints = glfwWindowHintsForBackend(backend_type); + hints.cocoa_retina_framebuffer = true; + if (options.headless) { + hints.visible = false; // Hiding window before creation otherwise you get the window showing up for a little bit then hiding. + } + + const monitors = try glfw.Monitor.getAll(allocator); + defer allocator.free(monitors); + var max_refresh_rate: u32 = 0; + for (monitors) |monitor| { + const video_mode = monitor.getVideoMode() orelse continue; + const refresh_rate = video_mode.getRefreshRate(); + max_refresh_rate = @max(max_refresh_rate, refresh_rate); + } + if (max_refresh_rate == 0) max_refresh_rate = 60; + frame.target = 2 * max_refresh_rate; + + const window = glfw.Window.create( + options.size.width, + options.size.height, + options.title, + null, + null, + hints, + ) orelse switch (glfw.mustGetErrorCode()) { + error.InvalidEnum, + error.InvalidValue, + error.FormatUnavailable, + => unreachable, + error.APIUnavailable, + error.VersionUnavailable, + error.PlatformError, + => |err| return err, + else => unreachable, + }; + + switch (backend_type) { + .opengl, .opengles => { + glfw.makeContextCurrent(window); + glfw.getErrorCode() catch |err| switch (err) { + error.PlatformError => return err, + else => unreachable, + }; + }, + else => {}, + } + + const instance = gpu.createInstance(null) orelse { + log.err("failed to create GPU instance", .{}); + std.process.exit(1); + }; + const surface = try createSurfaceForWindow(instance, window, comptime detectGLFWOptions()); + + var response: RequestAdapterResponse = undefined; + instance.requestAdapter(&gpu.RequestAdapterOptions{ + .compatible_surface = surface, + .power_preference = options.power_preference, + .force_fallback_adapter = .false, + }, &response, requestAdapterCallback); + if (response.status != .success) { + log.err("failed to create GPU adapter: {?s}", .{response.message}); + log.info("-> maybe try MACH_GPU_BACKEND=opengl ?", .{}); + std.process.exit(1); + } + + // Print which adapter we are going to use. + var props = std.mem.zeroes(gpu.Adapter.Properties); + response.adapter.?.getProperties(&props); + if (props.backend_type == .null) { + log.err("no backend found for {s} adapter", .{props.adapter_type.name()}); + std.process.exit(1); + } + log.info("found {s} backend on {s} adapter: {s}, {s}\n", .{ + props.backend_type.name(), + props.adapter_type.name(), + props.name, + props.driver_description, + }); + + // Create a device with default limits/features. + const gpu_device = response.adapter.?.createDevice(&.{ + .next_in_chain = .{ + .dawn_toggles_descriptor = &gpu.dawn.TogglesDescriptor.init(.{ + .enabled_toggles = &[_][*:0]const u8{ + "allow_unsafe_apis", + }, + }), + }, + + .required_features_count = if (options.required_features) |v| @as(u32, @intCast(v.len)) else 0, + .required_features = if (options.required_features) |v| @as(?[*]const gpu.FeatureName, v.ptr) else null, + .required_limits = if (options.required_limits) |limits| @as(?*const gpu.RequiredLimits, &gpu.RequiredLimits{ + .limits = limits, + }) else null, + .device_lost_callback = &deviceLostCallback, + .device_lost_userdata = null, + }) orelse { + log.err("failed to create GPU device\n", .{}); + std.process.exit(1); + }; + gpu_device.setUncapturedErrorCallback({}, printUnhandledErrorCallback); + + const framebuffer_size = window.getFramebufferSize(); + const swap_chain_desc = gpu.SwapChain.Descriptor{ + .label = "main swap chain", + .usage = .{ .render_attachment = true }, + .format = .bgra8_unorm, + .width = framebuffer_size.width, + .height = framebuffer_size.height, + .present_mode = .mailbox, + }; + const swap_chain = gpu_device.createSwapChain(surface, &swap_chain_desc); + + mach_core.adapter = response.adapter.?; + mach_core.device = gpu_device; + mach_core.queue = gpu_device.getQueue(); + mach_core.swap_chain = swap_chain; + mach_core.descriptor = swap_chain_desc; + + // The initial capacity we choose for the event queue is 2x our maximum expected event rate per + // frame. Specifically, 1000hz mouse updates are likely the maximum event rate we will encounter + // so we anticipate 2x that. If the event rate is higher than this per frame, it will grow to + // that maximum (we never shrink the event queue capacity in order to avoid allocations causing + // any stutter.) + var events = EventQueue.init(allocator); + try events.ensureTotalCapacity(2048); + + core.* = .{ + .allocator = allocator, + .frame = frame, + .input = input, + .window = window, + .backend_type = backend_type, + .user_ptr = undefined, + .instance = instance, + .surface = surface, + .gpu_adapter = response.adapter.?, + .gpu_device = gpu_device, + .max_refresh_rate = max_refresh_rate, + .swap_chain = swap_chain, + .swap_chain_desc = swap_chain_desc, + .events = events, + .current_title = undefined, + .current_border = undefined, + .last_border = undefined, + .current_headless = undefined, + .last_headless = undefined, + .current_size = undefined, + .last_size = undefined, + .last_windowed_size = window.getSize(), + .last_windowed_pos = window.getPos(), + .cursors = std.mem.zeroes([@typeInfo(CursorShape).Enum.fields.len]?glfw.Cursor), + .cursors_tried = std.mem.zeroes([@typeInfo(CursorShape).Enum.fields.len]bool), + .present_joysticks = std.StaticBitSet(@typeInfo(glfw.Joystick.Id).Enum.fields.len).initEmpty(), + }; + + core.current_title = options.title; + + core.current_display_mode = options.display_mode; + core.last_display_mode = .windowed; + + core.current_border = options.border; + core.last_border = true; + + core.current_headless = options.headless; + core.last_headless = core.current_headless; + + const actual_size = core.window.getSize(); + core.current_size = .{ .width = actual_size.width, .height = actual_size.height }; + core.last_size = core.current_size; + core.state_update.set(); + + core_instance = core; + core.user_ptr = .{ .self = core }; + + core.initCallbacks(); + + try core.input.start(); + + if (builtin.os.tag == .linux and !options.is_app and + core.linux_gamemode == null and try wantGamemode(core.allocator)) + core.linux_gamemode = initLinuxGamemode(); +} + +// Called on the main thread +fn initCallbacks(self: *Core) void { + self.window.setUserPointer(&self.user_ptr); + + const key_callback = struct { + fn callback(window: glfw.Window, key: glfw.Key, scancode: i32, action: glfw.Action, mods: glfw.Mods) void { + const pf = (window.getUserPointer(UserPtr) orelse unreachable).self; + const key_event = KeyEvent{ + .key = toMachKey(key), + .mods = toMachMods(mods), + }; + switch (action) { + .press => { + pf.input_mu.lock(); + pf.input_state.keys.set(@intFromEnum(key_event.key)); + pf.input_mu.unlock(); + pf.pushEvent(.{ .key_press = key_event }); + }, + .repeat => pf.pushEvent(.{ .key_repeat = key_event }), + .release => { + pf.input_mu.lock(); + pf.input_state.keys.unset(@intFromEnum(key_event.key)); + pf.input_mu.unlock(); + pf.pushEvent(.{ .key_release = key_event }); + }, + } + _ = scancode; + } + }.callback; + self.window.setKeyCallback(key_callback); + + const char_callback = struct { + fn callback(window: glfw.Window, codepoint: u21) void { + const pf = (window.getUserPointer(UserPtr) orelse unreachable).self; + pf.pushEvent(.{ + .char_input = .{ + .codepoint = codepoint, + }, + }); + } + }.callback; + self.window.setCharCallback(char_callback); + + const mouse_motion_callback = struct { + fn callback(window: glfw.Window, xpos: f64, ypos: f64) void { + const pf = (window.getUserPointer(UserPtr) orelse unreachable).self; + + pf.input_mu.lock(); + pf.input_state.mouse_position = .{ .x = xpos, .y = ypos }; + pf.input_mu.unlock(); + + pf.pushEvent(.{ + .mouse_motion = .{ + .pos = .{ + .x = xpos, + .y = ypos, + }, + }, + }); + } + }.callback; + self.window.setCursorPosCallback(mouse_motion_callback); + + const mouse_button_callback = struct { + fn callback(window: glfw.Window, button: glfw.mouse_button.MouseButton, action: glfw.Action, mods: glfw.Mods) void { + const pf = (window.getUserPointer(UserPtr) orelse unreachable).self; + const cursor_pos = pf.window.getCursorPos(); + const mouse_button_event = MouseButtonEvent{ + .button = toMachButton(button), + .pos = .{ .x = cursor_pos.xpos, .y = cursor_pos.ypos }, + .mods = toMachMods(mods), + }; + + pf.input_mu.lock(); + pf.input_state.mouse_position = mouse_button_event.pos; + pf.input_mu.unlock(); + + switch (action) { + .press => { + pf.input_mu.lock(); + pf.input_state.mouse_buttons.set(@intFromEnum(mouse_button_event.button)); + pf.input_mu.unlock(); + pf.pushEvent(.{ .mouse_press = mouse_button_event }); + }, + .release => { + pf.input_mu.lock(); + pf.input_state.mouse_buttons.unset(@intFromEnum(mouse_button_event.button)); + pf.input_mu.unlock(); + pf.pushEvent(.{ .mouse_release = mouse_button_event }); + }, + else => {}, + } + } + }.callback; + self.window.setMouseButtonCallback(mouse_button_callback); + + const scroll_callback = struct { + fn callback(window: glfw.Window, xoffset: f64, yoffset: f64) void { + const pf = (window.getUserPointer(UserPtr) orelse unreachable).self; + pf.pushEvent(.{ + .mouse_scroll = .{ + .xoffset = @as(f32, @floatCast(xoffset)), + .yoffset = @as(f32, @floatCast(yoffset)), + }, + }); + } + }.callback; + self.window.setScrollCallback(scroll_callback); + + const joystick_callback = struct { + fn callback(joystick: glfw.Joystick, event: glfw.Joystick.Event) void { + const pf = core_instance.?; + const idx: u8 = @intCast(@intFromEnum(joystick.jid)); + + switch (event) { + .connected => { + pf.input_mu.lock(); + pf.present_joysticks.set(idx); + pf.input_mu.unlock(); + pf.pushEvent(.{ + .joystick_connected = @enumFromInt(idx), + }); + }, + .disconnected => { + pf.input_mu.lock(); + pf.present_joysticks.unset(idx); + pf.input_mu.unlock(); + pf.pushEvent(.{ + .joystick_disconnected = @enumFromInt(idx), + }); + }, + } + } + }.callback; + glfw.Joystick.setCallback(joystick_callback); + + const close_callback = struct { + fn callback(window: glfw.Window) void { + window.setShouldClose(false); + const pf = (window.getUserPointer(UserPtr) orelse unreachable).self; + pf.pushEvent(.close); + } + }.callback; + self.window.setCloseCallback(close_callback); + + const focus_callback = struct { + fn callback(window: glfw.Window, focused: bool) void { + const pf = (window.getUserPointer(UserPtr) orelse unreachable).self; + pf.pushEvent(if (focused) .focus_gained else .focus_lost); + } + }.callback; + self.window.setFocusCallback(focus_callback); + + const framebuffer_size_callback = struct { + fn callback(window: glfw.Window, _: u32, _: u32) void { + const pf = (window.getUserPointer(UserPtr) orelse unreachable).self; + pf.swap_chain_update.set(); + } + }.callback; + self.window.setFramebufferSizeCallback(framebuffer_size_callback); + + const window_size_callback = struct { + fn callback(window: glfw.Window, width: i32, height: i32) void { + const pf = (window.getUserPointer(UserPtr) orelse unreachable).self; + pf.state_mu.lock(); + defer pf.state_mu.unlock(); + pf.current_size.width = @intCast(width); + pf.current_size.height = @intCast(height); + pf.last_size.width = @intCast(width); + pf.last_size.height = @intCast(height); + } + }.callback; + self.window.setSizeCallback(window_size_callback); +} + +fn pushEvent(self: *Core, event: Event) void { + self.events_mu.lock(); + defer self.events_mu.unlock(); + self.events.writeItem(event) catch self.oom.set(); +} + +// Called on the main thread +pub fn deinit(self: *Core) void { + for (self.cursors) |glfw_cursor| { + if (glfw_cursor) |cur| { + cur.destroy(); + } + } + self.events.deinit(); + + if (builtin.os.tag == .linux and + self.linux_gamemode != null and + self.linux_gamemode.?) + deinitLinuxGamemode(); + + self.gpu_device.setDeviceLostCallback(null, null); + + self.swap_chain.release(); + self.surface.release(); + mach_core.queue.release(); + self.gpu_device.release(); + self.gpu_adapter.release(); + self.instance.release(); +} + +// Secondary app-update thread +pub fn appUpdateThread(self: *Core, app: anytype) void { + self.frame.start() catch unreachable; + while (true) { + if (self.swap_chain_update.isSet()) blk: { + self.swap_chain_update.reset(); + + if (self.current_vsync_mode != self.last_vsync_mode) { + self.last_vsync_mode = self.current_vsync_mode; + switch (self.current_vsync_mode) { + .triple => self.frame.target = 2 * self.max_refresh_rate, + else => self.frame.target = 0, + } + } + + const framebuffer_size = self.window.getFramebufferSize(); + glfw.getErrorCode() catch break :blk; + if (framebuffer_size.width == 0 or framebuffer_size.height == 0) break :blk; + + { + self.swap_chain_mu.lock(); + defer self.swap_chain_mu.unlock(); + mach_core.swap_chain.release(); + self.swap_chain_desc.width = framebuffer_size.width; + self.swap_chain_desc.height = framebuffer_size.height; + self.swap_chain = self.gpu_device.createSwapChain(self.surface, &self.swap_chain_desc); + + mach_core.swap_chain = self.swap_chain; + mach_core.descriptor = self.swap_chain_desc; + } + + self.pushEvent(.{ + .framebuffer_resize = .{ + .width = framebuffer_size.width, + .height = framebuffer_size.height, + }, + }); + } + + if (app.update() catch unreachable) { + self.done.set(); + + // Wake the main thread from any event handling, so there is not e.g. a one second delay + // in exiting the application. + glfw.postEmptyEvent(); + return; + } + self.gpu_device.tick(); + self.gpu_device.machWaitForCommandsToBeScheduled(); + + self.frame.tick(); + if (self.frame.delay_ns != 0) std.time.sleep(self.frame.delay_ns); + } +} + +// Called on the main thread +pub fn update(self: *Core, app: anytype) !bool { + if (self.done.isSet()) return true; + if (!self.app_update_thread_started) { + self.app_update_thread_started = true; + const thread = try std.Thread.spawn(.{}, appUpdateThread, .{ self, app }); + thread.detach(); + } + + if (self.state_update.isSet()) { + self.state_update.reset(); + + // Title changes + if (self.current_title_changed) { + self.current_title_changed = false; + self.window.setTitle(self.current_title); + } + + // Display mode changes + if (self.current_display_mode != self.last_display_mode) { + const current_border = self.current_border; + switch (self.current_display_mode) { + .windowed => { + self.window.setAttrib(.decorated, current_border); + self.window.setAttrib(.floating, false); + self.window.setMonitor( + null, + @intCast(self.last_windowed_pos.x), + @intCast(self.last_windowed_pos.y), + self.last_size.width, + self.last_size.height, + null, + ); + }, + .fullscreen => { + if (self.last_display_mode == .windowed) { + self.last_windowed_size = self.window.getSize(); + self.last_windowed_pos = self.window.getPos(); + } + + if (glfw.Monitor.getPrimary()) |monitor| { + if (monitor.getVideoMode()) |video_mode| { + self.window.setAttrib(.decorated, false); + self.window.setAttrib(.floating, true); + self.window.setMonitor(null, 0, 0, video_mode.getWidth(), video_mode.getHeight(), null); + } + } + }, + .borderless => { + if (self.last_display_mode == .windowed) { + self.last_windowed_size = self.window.getSize(); + self.last_windowed_pos = self.window.getPos(); + } + + self.window.setAttrib(.decorated, false); + self.window.setAttrib(.floating, true); + + if (glfw.Monitor.getPrimary()) |monitor| { + if (monitor.getVideoMode()) |video_mode| { + self.window.setAttrib(.decorated, false); + self.window.setAttrib(.floating, true); + self.window.setMonitor(null, 0, 0, video_mode.getWidth(), video_mode.getHeight(), null); + } + } + }, + } + self.last_display_mode = self.current_display_mode; + } + + // Border changes + if (self.current_border != self.last_border) { + self.last_border = self.current_border; + if (self.current_display_mode != .borderless) self.window.setAttrib(.decorated, self.current_border); + } + + // Headless changes + if (self.current_headless != self.last_headless) { + self.current_headless = self.last_headless; + if (self.current_headless) self.window.hide() else self.window.show(); + } + + // Size changes + if (!self.current_size.eql(self.last_size)) { + self.last_size = self.current_size; + self.window.setSize(.{ + .width = self.current_size.width, + .height = self.current_size.height, + }); + } + + // Size limit changes + if (!self.current_size_limit.eql(self.last_size_limit)) { + self.last_size_limit = self.current_size_limit; + self.window.setSizeLimits( + .{ .width = self.current_size_limit.min.width, .height = self.current_size_limit.min.height }, + .{ .width = self.current_size_limit.max.width, .height = self.current_size_limit.max.height }, + ); + } + + // Cursor mode changes + if (self.current_cursor_mode != self.last_cursor_mode) { + self.last_cursor_mode = self.current_cursor_mode; + self.window.setInputModeCursor(switch (self.current_cursor_mode) { + .normal => .normal, + .hidden => .hidden, + .disabled => .disabled, + }); + // on e.g. macOS raw mouse motion is not supported. If an error occurs here, there is + // nothing meaningful we can do anyway so just silence the warning. + glfw.getErrorCode() catch {}; + } + + // Cursor shape changes + if (self.current_cursor_shape != self.last_cursor_shape) { + self.last_cursor_shape = self.current_cursor_shape; + // TODO(feature): creating a GLFW standard cursor could fail, we should provide custom backup + // images for these. https://github.com/hexops/mach/pull/352 + const enum_int = @intFromEnum(self.current_cursor_shape); + const tried = self.cursors_tried[enum_int]; + if (!tried) { + self.cursors_tried[enum_int] = true; + self.cursors[enum_int] = switch (self.current_cursor_shape) { + .arrow => glfw.Cursor.createStandard(.arrow), + .ibeam => glfw.Cursor.createStandard(.ibeam), + .crosshair => glfw.Cursor.createStandard(.crosshair), + .pointing_hand => glfw.Cursor.createStandard(.pointing_hand), + .resize_ew => glfw.Cursor.createStandard(.resize_ew), + .resize_ns => glfw.Cursor.createStandard(.resize_ns), + .resize_nwse => glfw.Cursor.createStandard(.resize_nwse), + .resize_nesw => glfw.Cursor.createStandard(.resize_nesw), + .resize_all => glfw.Cursor.createStandard(.resize_all), + .not_allowed => glfw.Cursor.createStandard(.not_allowed), + }; + } + + if (self.cursors[enum_int]) |cur| { + self.window.setCursor(cur); + } else { + glfw.getErrorCode() catch {}; // discard error + // TODO(feature): creating a GLFW standard cursor could fail, we should provide custom backup + // images for these. https://github.com/hexops/mach/pull/352 + log.warn("mach: setCursorShape: {s} not yet supported\n", .{@tagName(self.current_cursor_shape)}); + } + } + } + + const frequency_delay = @as(f32, @floatFromInt(self.input.delay_ns)) / @as(f32, @floatFromInt(std.time.ns_per_s)); + glfw.waitEventsTimeout(frequency_delay); + + if (@hasDecl(std.meta.Child(@TypeOf(app)), "updateMainThread")) { + if (app.updateMainThread() catch unreachable) { + self.done.set(); + return true; + } + } + + glfw.getErrorCode() catch |err| switch (err) { + error.PlatformError => log.err("glfw: failed to poll events", .{}), + error.InvalidValue => unreachable, + else => unreachable, + }; + self.input.tick(); + return false; +} + +// May be called from any thread. +pub inline fn pollEvents(self: *Core) EventIterator { + return EventIterator{ .events_mu = &self.events_mu, .queue = &self.events }; +} + +// May be called from any thread. +pub fn setTitle(self: *Core, title: [:0]const u8) void { + self.state_mu.lock(); + defer self.state_mu.unlock(); + self.current_title = title; + self.current_title_changed = true; + self.state_update.set(); + self.wakeMainThread(); +} + +// May be called from any thread. +pub fn setDisplayMode(self: *Core, mode: DisplayMode) void { + self.state_mu.lock(); + defer self.state_mu.unlock(); + self.current_display_mode = mode; + if (self.current_display_mode != self.last_display_mode) { + self.state_update.set(); + self.wakeMainThread(); + } +} + +// May be called from any thread. +pub fn displayMode(self: *Core) DisplayMode { + self.state_mu.lock(); + defer self.state_mu.unlock(); + return self.current_display_mode; +} + +// May be called from any thread. +pub fn setBorder(self: *Core, value: bool) void { + self.state_mu.lock(); + defer self.state_mu.unlock(); + self.current_border = value; + if (self.current_border != self.last_border) { + self.state_update.set(); + self.wakeMainThread(); + } +} + +// May be called from any thread. +pub fn border(self: *Core) bool { + self.state_mu.lock(); + defer self.state_mu.unlock(); + return self.current_border; +} + +// May be called from any thread. +pub fn setHeadless(self: *Core, value: bool) void { + self.state_mu.lock(); + defer self.state_mu.unlock(); + self.current_headless = value; + if (self.current_headless != self.last_headless) { + self.state_update.set(); + self.wakeMainThread(); + } +} + +// May be called from any thread. +pub fn headless(self: *Core) bool { + self.state_mu.lock(); + defer self.state_mu.unlock(); + return self.current_headless; +} + +// May be called from any thread. +pub fn setVSync(self: *Core, mode: VSyncMode) void { + self.swap_chain_mu.lock(); + self.swap_chain_desc.present_mode = switch (mode) { + .none => .immediate, + .double => .fifo, + .triple => .mailbox, + }; + self.current_vsync_mode = mode; + self.swap_chain_mu.unlock(); + self.swap_chain_update.set(); + self.wakeMainThread(); +} + +// May be called from any thread. +pub fn vsync(self: *Core) VSyncMode { + self.swap_chain_mu.lockShared(); + defer self.swap_chain_mu.unlockShared(); + return switch (self.swap_chain_desc.present_mode) { + .immediate => .none, + .fifo => .double, + .mailbox => .triple, + }; +} + +// May be called from any thread. +pub fn setSize(self: *Core, value: Size) void { + self.state_mu.lock(); + defer self.state_mu.unlock(); + self.current_size = value; + if (!self.current_size.eql(self.last_size)) { + self.state_update.set(); + self.wakeMainThread(); + } +} + +// May be called from any thread. +pub fn size(self: *Core) Size { + self.state_mu.lock(); + defer self.state_mu.unlock(); + return self.current_size; +} + +// May be called from any thread. +pub fn setSizeLimit(self: *Core, limit: SizeLimit) void { + self.state_mu.lock(); + defer self.state_mu.unlock(); + self.current_size_limit = limit; + if (!self.current_size_limit.eql(self.last_size_limit)) { + self.state_update.set(); + self.wakeMainThread(); + } +} + +// May be called from any thread. +pub fn sizeLimit(self: *Core) SizeLimit { + self.state_mu.lock(); + defer self.state_mu.unlock(); + return self.current_size_limit; +} + +// May be called from any thread. +pub fn setCursorMode(self: *Core, mode: CursorMode) void { + self.state_mu.lock(); + defer self.state_mu.unlock(); + self.current_cursor_mode = mode; + if (self.current_cursor_mode != self.last_cursor_mode) { + self.state_update.set(); + self.wakeMainThread(); + } +} + +// May be called from any thread. +pub fn cursorMode(self: *Core) CursorMode { + self.state_mu.lock(); + defer self.state_mu.unlock(); + return self.current_cursor_mode; +} + +// May be called from any thread. +pub fn setCursorShape(self: *Core, shape: CursorShape) void { + self.state_mu.lock(); + defer self.state_mu.unlock(); + self.current_cursor_shape = shape; + if (self.current_cursor_shape != self.last_cursor_shape) { + self.state_update.set(); + self.wakeMainThread(); + } +} + +// May be called from any thread. +pub fn cursorShape(self: *Core) CursorShape { + self.state_mu.lock(); + defer self.state_mu.unlock(); + return self.current_cursor_shape; +} + +// May be called from any thread. +pub fn joystickPresent(_: *Core, _: Joystick) bool { + @panic("TODO: thread safe API"); + // const idx: u8 = @intFromEnum(joystick); + // if (idx >= @typeInfo(glfw.Joystick.Id).Enum.len) return false; + + // self.input_mu.lockShared(); + // defer self.input_mu.unlockShared(); + // return self.present_joysticks.isSet(idx); +} + +// May be called from any thread. +pub fn joystickName(_: *Core, _: Joystick) ?[:0]const u8 { + @panic("TODO: thread safe API"); + // const idx: u8 = @intFromEnum(joystick); + // if (idx >= @typeInfo(glfw.Joystick.Id).Enum.len) return null; + + // const glfw_joystick = glfw.Joystick{ .jid = @intCast(idx) }; + // return glfw_joystick.getName(); +} + +// May be called from any thread. +pub fn joystickButtons(_: *Core, _: Joystick) ?[]const bool { + @panic("TODO: thread safe API"); + // const idx: u8 = @intFromEnum(joystick); + // if (idx >= @typeInfo(glfw.Joystick.Id).Enum.len) return null; + + // const glfw_joystick = glfw.Joystick{ .jid = @intCast(idx) }; + // return @ptrCast(glfw_joystick.getButtons()); +} + +// May be called from any thread. +pub fn joystickAxes(_: *Core, _: Joystick) ?[]const f32 { + @panic("TODO: thread safe API"); + // const idx: u8 = @intFromEnum(joystick); + // if (idx >= @typeInfo(glfw.Joystick.Id).Enum.len) return null; + + // const glfw_joystick = glfw.Joystick{ .jid = @intCast(idx) }; + // return glfw_joystick.getAxes(); +} + +// May be called from any thread. +pub fn keyPressed(self: *Core, key: Key) bool { + self.input_mu.lockShared(); + defer self.input_mu.unlockShared(); + return self.input_state.isKeyPressed(key); +} + +// May be called from any thread. +pub fn keyReleased(self: *Core, key: Key) bool { + self.input_mu.lockShared(); + defer self.input_mu.unlockShared(); + return self.input_state.isKeyReleased(key); +} + +// May be called from any thread. +pub fn mousePressed(self: *Core, button: MouseButton) bool { + self.input_mu.lockShared(); + defer self.input_mu.unlockShared(); + return self.input_state.isMouseButtonPressed(button); +} + +// May be called from any thread. +pub fn mouseReleased(self: *Core, button: MouseButton) bool { + self.input_mu.lockShared(); + defer self.input_mu.unlockShared(); + return self.input_state.isMouseButtonReleased(button); +} + +// May be called from any thread. +pub fn mousePosition(self: *Core) mach_core.Position { + self.input_mu.lockShared(); + defer self.input_mu.unlockShared(); + return self.input_state.mouse_position; +} + +// May be called from any thread. +pub inline fn outOfMemory(self: *Core) bool { + if (self.oom.isSet()) { + self.oom.reset(); + return true; + } + return false; +} + +// May be called from any thread. +pub inline fn wakeMainThread(self: *Core) void { + _ = self; + glfw.postEmptyEvent(); +} + +// May be called from any thread. +pub fn nativeWindowCocoa(self: *Core) *anyopaque { + const glfw_native = glfw.Native(comptime detectGLFWOptions()); + return glfw_native.getCocoaWindow(self.window).?; +} + +// May be called from any thread. +pub fn nativeWindowWin32(self: *Core) std.os.windows.HWND { + const glfw_native = glfw.Native(comptime detectGLFWOptions()); + return glfw_native.getWin32Window(self.window); +} + +fn toMachButton(button: glfw.mouse_button.MouseButton) MouseButton { + return switch (button) { + .left => .left, + .right => .right, + .middle => .middle, + .four => .four, + .five => .five, + .six => .six, + .seven => .seven, + .eight => .eight, + }; +} + +fn toMachKey(key: glfw.Key) Key { + return switch (key) { + .a => .a, + .b => .b, + .c => .c, + .d => .d, + .e => .e, + .f => .f, + .g => .g, + .h => .h, + .i => .i, + .j => .j, + .k => .k, + .l => .l, + .m => .m, + .n => .n, + .o => .o, + .p => .p, + .q => .q, + .r => .r, + .s => .s, + .t => .t, + .u => .u, + .v => .v, + .w => .w, + .x => .x, + .y => .y, + .z => .z, + + .zero => .zero, + .one => .one, + .two => .two, + .three => .three, + .four => .four, + .five => .five, + .six => .six, + .seven => .seven, + .eight => .eight, + .nine => .nine, + + .F1 => .f1, + .F2 => .f2, + .F3 => .f3, + .F4 => .f4, + .F5 => .f5, + .F6 => .f6, + .F7 => .f7, + .F8 => .f8, + .F9 => .f9, + .F10 => .f10, + .F11 => .f11, + .F12 => .f12, + .F13 => .f13, + .F14 => .f14, + .F15 => .f15, + .F16 => .f16, + .F17 => .f17, + .F18 => .f18, + .F19 => .f19, + .F20 => .f20, + .F21 => .f21, + .F22 => .f22, + .F23 => .f23, + .F24 => .f24, + .F25 => .f25, + + .kp_divide => .kp_divide, + .kp_multiply => .kp_multiply, + .kp_subtract => .kp_subtract, + .kp_add => .kp_add, + .kp_0 => .kp_0, + .kp_1 => .kp_1, + .kp_2 => .kp_2, + .kp_3 => .kp_3, + .kp_4 => .kp_4, + .kp_5 => .kp_5, + .kp_6 => .kp_6, + .kp_7 => .kp_7, + .kp_8 => .kp_8, + .kp_9 => .kp_9, + .kp_decimal => .kp_decimal, + .kp_equal => .kp_equal, + .kp_enter => .kp_enter, + + .enter => .enter, + .escape => .escape, + .tab => .tab, + .left_shift => .left_shift, + .right_shift => .right_shift, + .left_control => .left_control, + .right_control => .right_control, + .left_alt => .left_alt, + .right_alt => .right_alt, + .left_super => .left_super, + .right_super => .right_super, + .menu => .menu, + .num_lock => .num_lock, + .caps_lock => .caps_lock, + .print_screen => .print, + .scroll_lock => .scroll_lock, + .pause => .pause, + .delete => .delete, + .home => .home, + .end => .end, + .page_up => .page_up, + .page_down => .page_down, + .insert => .insert, + .left => .left, + .right => .right, + .up => .up, + .down => .down, + .backspace => .backspace, + .space => .space, + .minus => .minus, + .equal => .equal, + .left_bracket => .left_bracket, + .right_bracket => .right_bracket, + .backslash => .backslash, + .semicolon => .semicolon, + .apostrophe => .apostrophe, + .comma => .comma, + .period => .period, + .slash => .slash, + .grave_accent => .grave, + + .world_1 => .unknown, + .world_2 => .unknown, + .unknown => .unknown, + }; +} + +fn toMachMods(mods: glfw.Mods) KeyMods { + return .{ + .shift = mods.shift, + .control = mods.control, + .alt = mods.alt, + .super = mods.super, + .caps_lock = mods.caps_lock, + .num_lock = mods.num_lock, + }; +} + +/// GLFW error handling callback +/// +/// This only logs errors, and doesn't e.g. exit the application, because many simple operations of +/// GLFW can result in an error on the stack when running under different Wayland Linux systems. +/// Doing anything else here would result in a good chance of applications not working on Wayland, +/// so the best thing to do really is to just log the error. See the mach-glfw README for more info. +fn errorCallback(error_code: glfw.ErrorCode, description: [:0]const u8) void { + if (std.mem.eql(u8, description, "Raw mouse motion is not supported on this system")) return; + log.err("glfw: {}: {s}\n", .{ error_code, description }); +} + +fn glfwWindowHintsForBackend(backend: gpu.BackendType) glfw.Window.Hints { + return switch (backend) { + .opengl => .{ + // Ask for OpenGL 4.4 which is what the GL backend requires for compute shaders and + // texture views. + .context_version_major = 4, + .context_version_minor = 4, + .opengl_forward_compat = true, + .opengl_profile = .opengl_core_profile, + }, + .opengles => .{ + .context_version_major = 3, + .context_version_minor = 1, + .client_api = .opengl_es_api, + .context_creation_api = .egl_context_api, + }, + else => .{ + // Without this GLFW will initialize a GL context on the window, which prevents using + // the window with other APIs (by crashing in weird ways). + .client_api = .no_api, + }, + }; +} + +fn detectGLFWOptions() glfw.BackendOptions { + if (builtin.target.isDarwin()) return .{ .cocoa = true }; + return switch (builtin.target.os.tag) { + .windows => .{ .win32 = true }, + .linux => .{ .x11 = true, .wayland = true }, + else => .{}, + }; +} + +fn createSurfaceForWindow( + instance: *gpu.Instance, + window: glfw.Window, + comptime glfw_options: glfw.BackendOptions, +) !*gpu.Surface { + const glfw_native = glfw.Native(glfw_options); + const extension = if (glfw_options.win32) gpu.Surface.Descriptor.NextInChain{ + .from_windows_hwnd = &.{ + .hinstance = std.os.windows.kernel32.GetModuleHandleW(null).?, + .hwnd = glfw_native.getWin32Window(window), + }, + } else if (glfw_options.x11 or glfw_options.wayland) blk: { + break :blk switch (glfw.getPlatform()) { + .wayland => gpu.Surface.Descriptor.NextInChain{ + .from_wayland_surface = &.{ + .display = glfw_native.getWaylandDisplay(), + .surface = glfw_native.getWaylandWindow(window), + }, + }, + .x11 => gpu.Surface.Descriptor.NextInChain{ + .from_xlib_window = &.{ + .display = glfw_native.getX11Display(), + .window = glfw_native.getX11Window(window), + }, + }, + else => unreachable, + }; + } else if (glfw_options.cocoa) blk: { + const pool = try objc.AutoReleasePool.init(); + defer objc.AutoReleasePool.release(pool); + + const ns_window = glfw_native.getCocoaWindow(window); + const ns_view = objc.msgSend(ns_window, "contentView", .{}, *anyopaque); // [nsWindow contentView] + + // Create a CAMetalLayer that covers the whole window that will be passed to CreateSurface. + objc.msgSend(ns_view, "setWantsLayer:", .{true}, void); // [view setWantsLayer:YES] + const layer = objc.msgSend(objc.objc_getClass("CAMetalLayer"), "layer", .{}, ?*anyopaque); // [CAMetalLayer layer] + if (layer == null) @panic("failed to create Metal layer"); + objc.msgSend(ns_view, "setLayer:", .{layer.?}, void); // [view setLayer:layer] + + // Use retina if the window was created with retina support. + const scale_factor = objc.msgSend(ns_window, "backingScaleFactor", .{}, f64); // [ns_window backingScaleFactor] + objc.msgSend(layer.?, "setContentsScale:", .{scale_factor}, void); // [layer setContentsScale:scale_factor] + + break :blk gpu.Surface.Descriptor.NextInChain{ .from_metal_layer = &.{ .layer = layer.? } }; + } else unreachable; + + return instance.createSurface(&gpu.Surface.Descriptor{ + .next_in_chain = extension, + }); +} + +inline fn printUnhandledErrorCallback(_: void, ty: gpu.ErrorType, message: [*:0]const u8) void { + switch (ty) { + .validation => std.log.err("gpu: validation error: {s}\n", .{message}), + .out_of_memory => std.log.err("gpu: out of memory: {s}\n", .{message}), + .device_lost => std.log.err("gpu: device lost: {s}\n", .{message}), + .unknown => std.log.err("gpu: unknown error: {s}\n", .{message}), + else => unreachable, + } + std.os.exit(1); +} + +fn detectBackendType(allocator: std.mem.Allocator) !gpu.BackendType { + const backend = std.process.getEnvVarOwned( + allocator, + "MACH_GPU_BACKEND", + ) catch |err| switch (err) { + error.EnvironmentVariableNotFound => { + if (builtin.target.isDarwin()) return .metal; + if (builtin.target.os.tag == .windows) return .d3d12; + return .vulkan; + }, + else => return err, + }; + defer allocator.free(backend); + + if (std.ascii.eqlIgnoreCase(backend, "null")) return .null; + if (std.ascii.eqlIgnoreCase(backend, "d3d11")) return .d3d11; + if (std.ascii.eqlIgnoreCase(backend, "d3d12")) return .d3d12; + if (std.ascii.eqlIgnoreCase(backend, "metal")) return .metal; + if (std.ascii.eqlIgnoreCase(backend, "vulkan")) return .vulkan; + if (std.ascii.eqlIgnoreCase(backend, "opengl")) return .opengl; + if (std.ascii.eqlIgnoreCase(backend, "opengles")) return .opengles; + + @panic("unknown MACH_GPU_BACKEND type"); +} + +/// Check if gamemode should be activated +fn wantGamemode(allocator: std.mem.Allocator) error{ OutOfMemory, InvalidUtf8 }!bool { + const use_gamemode = std.process.getEnvVarOwned( + allocator, + "MACH_USE_GAMEMODE", + ) catch |err| switch (err) { + error.EnvironmentVariableNotFound => return true, + else => |e| return e, + }; + defer allocator.free(use_gamemode); + + return !(std.ascii.eqlIgnoreCase(use_gamemode, "off") or std.ascii.eqlIgnoreCase(use_gamemode, "false")); +} + +fn initLinuxGamemode() bool { + const gamemode = @import("mach-gamemode"); + gamemode.start(); + if (!gamemode.isActive()) return false; + log.info("gamemode: activated", .{}); + return true; +} + +fn deinitLinuxGamemode() void { + const gamemode = @import("mach-gamemode"); + gamemode.stop(); +} + +const RequestAdapterResponse = struct { + status: gpu.RequestAdapterStatus, + adapter: ?*gpu.Adapter, + message: ?[*:0]const u8, +}; + +inline fn requestAdapterCallback( + context: *RequestAdapterResponse, + status: gpu.RequestAdapterStatus, + adapter: ?*gpu.Adapter, + message: ?[*:0]const u8, +) void { + context.* = RequestAdapterResponse{ + .status = status, + .adapter = adapter, + .message = message, + }; +} diff --git a/src/core/platform/glfw/objc.zig b/src/core/platform/glfw/objc.zig new file mode 100644 index 00000000..b74888ae --- /dev/null +++ b/src/core/platform/glfw/objc.zig @@ -0,0 +1,57 @@ +const builtin = @import("builtin"); + +// Extracted from `zig translate-c tmp.c` with `#include ` in the file. +pub const SEL = opaque {}; +pub const Class = opaque {}; + +pub extern fn sel_getUid(str: [*c]const u8) ?*SEL; +pub extern fn objc_getClass(name: [*c]const u8) ?*Class; +pub extern fn objc_msgSend() void; + +pub const AutoReleasePool = if (!builtin.target.isDarwin()) opaque { + pub fn init() error{OutOfMemory}!?*AutoReleasePool { + return null; + } + + pub fn release(pool: ?*AutoReleasePool) void { + _ = pool; + return; + } +} else opaque { + pub fn init() error{OutOfMemory}!?*AutoReleasePool { + // pool = [NSAutoreleasePool alloc]; + var pool = msgSend(objc_getClass("NSAutoreleasePool"), "alloc", .{}, ?*AutoReleasePool); + if (pool == null) return error.OutOfMemory; + + // pool = [pool init]; + pool = msgSend(pool, "init", .{}, ?*AutoReleasePool); + if (pool == null) unreachable; + + return pool; + } + + pub fn release(pool: ?*AutoReleasePool) void { + // [pool release]; + msgSend(pool, "release", .{}, void); + } +}; + +// Borrowed from https://github.com/hazeycode/zig-objcrt +pub fn msgSend(obj: anytype, sel_name: [:0]const u8, args: anytype, comptime ReturnType: type) ReturnType { + const args_meta = @typeInfo(@TypeOf(args)).Struct.fields; + + const FnType = switch (args_meta.len) { + 0 => *const fn (@TypeOf(obj), ?*SEL) callconv(.C) ReturnType, + 1 => *const fn (@TypeOf(obj), ?*SEL, args_meta[0].type) callconv(.C) ReturnType, + 2 => *const fn (@TypeOf(obj), ?*SEL, args_meta[0].type, args_meta[1].type) callconv(.C) ReturnType, + 3 => *const fn (@TypeOf(obj), ?*SEL, args_meta[0].type, args_meta[1].type, args_meta[2].type) callconv(.C) ReturnType, + 4 => *const fn (@TypeOf(obj), ?*SEL, args_meta[0].type, args_meta[1].type, args_meta[2].type, args_meta[3].type) callconv(.C) ReturnType, + else => @compileError("Unsupported number of args"), + }; + + // NOTE: func is a var because making it const causes a compile error which I believe is a compiler bug + const func = @as(FnType, @ptrCast(&objc_msgSend)); + const sel = sel_getUid(@as([*c]const u8, @ptrCast(sel_name))); + + return @call(.auto, func, .{ obj, sel } ++ args); +} diff --git a/src/core/platform/native_entrypoint.zig b/src/core/platform/native_entrypoint.zig new file mode 100644 index 00000000..da55567c --- /dev/null +++ b/src/core/platform/native_entrypoint.zig @@ -0,0 +1,39 @@ +// Check that the user's app matches the required interface. +comptime { + if (!@import("builtin").is_test) @import("mach").core.AppInterface(@import("app")); +} + +// Forward "app" declarations into our namespace, such that @import("root").foo works as expected. +pub usingnamespace @import("app"); +const App = @import("app").App; + +const std = @import("std"); +const core = @import("mach").core; + +pub usingnamespace if (!@hasDecl(App, "GPUInterface")) struct { + pub const GPUInterface = core.wgpu.dawn.Interface; +} else struct {}; + +pub usingnamespace if (!@hasDecl(App, "SYSGPUInterface")) extern struct { + pub const SYSGPUInterface = core.sysgpu.Impl; +} else struct {}; + +pub fn main() !void { + // Run from the directory where the executable is located so relative assets can be found. + var buffer: [1024]u8 = undefined; + const path = std.fs.selfExeDirPath(buffer[0..]) catch "."; + std.os.chdir(path) catch {}; + + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + core.allocator = gpa.allocator(); + + // Initialize GPU implementation + if (comptime core.options.use_wgpu) try core.wgpu.Impl.init(core.allocator, .{}); + if (comptime core.options.use_sysgpu) try core.sysgpu.Impl.init(core.allocator, .{}); + + var app: App = undefined; + try app.init(); + defer app.deinit(); + while (!try core.update(&app)) {} +} diff --git a/src/core/platform/wasm.zig b/src/core/platform/wasm.zig new file mode 100644 index 00000000..79f799f9 --- /dev/null +++ b/src/core/platform/wasm.zig @@ -0,0 +1,2 @@ +pub const Core = @import("wasm/Core.zig"); +pub const Timer = @import("wasm/Timer.zig"); diff --git a/src/core/platform/wasm/Core.zig b/src/core/platform/wasm/Core.zig new file mode 100644 index 00000000..1060802d --- /dev/null +++ b/src/core/platform/wasm/Core.zig @@ -0,0 +1,478 @@ +const std = @import("std"); +const js = @import("js.zig"); +const Timer = @import("Timer.zig"); +const mach_core = @import("../../main.zig"); +const gpu = mach_core.gpu; +const Options = @import("../../main.zig").Options; +const Event = @import("../../main.zig").Event; +const KeyEvent = @import("../../main.zig").KeyEvent; +const MouseButtonEvent = @import("../../main.zig").MouseButtonEvent; +const MouseButton = @import("../../main.zig").MouseButton; +const Size = @import("../../main.zig").Size; +const Position = @import("../../main.zig").Position; +const DisplayMode = @import("../../main.zig").DisplayMode; +const SizeLimit = @import("../../main.zig").SizeLimit; +const CursorShape = @import("../../main.zig").CursorShape; +const VSyncMode = @import("../../main.zig").VSyncMode; +const CursorMode = @import("../../main.zig").CursorMode; +const Key = @import("../../main.zig").Key; +const KeyMods = @import("../../main.zig").KeyMods; +const Joystick = @import("../../main.zig").Joystick; +const InputState = @import("../../InputState.zig"); +const Frequency = @import("../../Frequency.zig"); + +// Custom std.log implementation which logs to the browser console. +pub fn defaultLog( + comptime message_level: std.log.Level, + comptime scope: @Type(.EnumLiteral), + comptime format: []const u8, + args: anytype, +) void { + const prefix = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): "; + const writer = LogWriter{ .context = {} }; + + writer.print(message_level.asText() ++ prefix ++ format ++ "\n", args) catch return; + machLogFlush(); +} + +// Custom @panic implementation which logs to the browser console. +pub fn defaultPanic(msg: []const u8, error_return_trace: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn { + _ = error_return_trace; + _ = ret_addr; + machPanic(msg.ptr, msg.len); + unreachable; +} + +pub extern "mach" fn machPanic(str: [*]const u8, len: u32) void; +pub extern "mach" fn machLogWrite(str: [*]const u8, len: u32) void; +pub extern "mach" fn machLogFlush() void; + +const LogError = error{}; +const LogWriter = std.io.Writer(void, LogError, writeLog); +fn writeLog(_: void, msg: []const u8) LogError!usize { + machLogWrite(msg.ptr, msg.len); + return msg.len; +} + +pub const Core = @This(); + +allocator: std.mem.Allocator, +frame: *Frequency, +input: *Frequency, +id: js.CanvasId, + +input_state: InputState, +joysticks: [JoystickData.max_joysticks]JoystickData, + +pub const EventIterator = struct { + core: *Core, + + pub inline fn next(self: *EventIterator) ?Event { + while (true) { + const event_int = js.machEventShift(); + if (event_int == -1) return null; + + const event_type = @as(std.meta.Tag(Event), @enumFromInt(event_int)); + return switch (event_type) { + .key_press, .key_repeat, .key_release => blk: { + const key = @as(Key, @enumFromInt(js.machEventShift())); + + switch (event_type) { + .key_press => { + self.core.input_state.keys.set(@intFromEnum(key)); + break :blk Event{ + .key_press = .{ + .key = key, + .mods = self.makeKeyMods(), + }, + }; + }, + .key_repeat => break :blk Event{ + .key_repeat = .{ + .key = key, + .mods = self.makeKeyMods(), + }, + }, + .key_release => { + self.core.input_state.keys.unset(@intFromEnum(key)); + break :blk Event{ + .key_release = .{ + .key = key, + .mods = self.makeKeyMods(), + }, + }; + }, + else => unreachable, + } + + continue; + }, + .mouse_motion => blk: { + const x = @as(f64, @floatFromInt(js.machEventShift())); + const y = @as(f64, @floatFromInt(js.machEventShift())); + + self.core.input_state.mouse_position = .{ .x = x, .y = y }; + + break :blk Event{ + .mouse_motion = .{ + .pos = .{ + .x = x, + .y = y, + }, + }, + }; + }, + .mouse_press => blk: { + const button = toMachButton(js.machEventShift()); + self.core.input_state.mouse_buttons.set(@intFromEnum(button)); + + break :blk Event{ + .mouse_press = .{ + .button = button, + .pos = self.core.input_state.mouse_position, + .mods = self.makeKeyMods(), + }, + }; + }, + .mouse_release => blk: { + const button = toMachButton(js.machEventShift()); + self.core.input_state.mouse_buttons.unset(@intFromEnum(button)); + + break :blk Event{ + .mouse_release = .{ + .button = button, + .pos = self.core.input_state.mouse_position, + .mods = self.makeKeyMods(), + }, + }; + }, + .mouse_scroll => Event{ + .mouse_scroll = .{ + .xoffset = @as(f32, @floatCast(std.math.sign(js.machEventShiftFloat()))), + .yoffset = @as(f32, @floatCast(std.math.sign(js.machEventShiftFloat()))), + }, + }, + .joystick_connected => blk: { + const idx: u8 = @intCast(js.machEventShift()); + const btn_count: usize = @intCast(js.machEventShift()); + const axis_count: usize = @intCast(js.machEventShift()); + if (idx >= JoystickData.max_joysticks) continue; + + var data = &self.core.joysticks[idx]; + data.present = true; + data.button_count = @min(JoystickData.max_button_count, btn_count); + data.axis_count = @min(JoystickData.max_axis_count, axis_count); + + js.machJoystickName(idx, &data.name, JoystickData.max_name_len); + + break :blk Event{ .joystick_connected = @enumFromInt(idx) }; + }, + .joystick_disconnected => blk: { + const idx: u8 = @intCast(js.machEventShift()); + if (idx >= JoystickData.max_joysticks) continue; + + var data = &self.core.joysticks[idx]; + data.present = false; + data.button_count = 0; + data.axis_count = 0; + + @memset(&data.buttons, false); + @memset(&data.axes, 0); + + break :blk Event{ .joystick_disconnected = @enumFromInt(idx) }; + }, + .framebuffer_resize => blk: { + const width = @as(u32, @intCast(js.machEventShift())); + const height = @as(u32, @intCast(js.machEventShift())); + const pixel_ratio = @as(u32, @intCast(js.machEventShift())); + break :blk Event{ + .framebuffer_resize = .{ + .width = width * pixel_ratio, + .height = height * pixel_ratio, + }, + }; + }, + .focus_gained => Event.focus_gained, + .focus_lost => Event.focus_lost, + else => null, + }; + } + } + + fn makeKeyMods(self: EventIterator) KeyMods { + const is = self.core.input_state; + + return .{ + .shift = is.isKeyPressed(.left_shift) or is.isKeyPressed(.right_shift), + .control = is.isKeyPressed(.left_control) or is.isKeyPressed(.right_control), + .alt = is.isKeyPressed(.left_alt) or is.isKeyPressed(.right_alt), + .super = is.isKeyPressed(.left_super) or is.isKeyPressed(.right_super), + // FIXME(estel): I think the logic for these two are wrong, but unlikely it matters + // in a browser. To correct them we need to actually use `KeyboardEvent.getModifierState` + // in javascript and bring back that info in here. + .caps_lock = is.isKeyPressed(.caps_lock), + .num_lock = is.isKeyPressed(.num_lock), + }; + } +}; + +const JoystickData = struct { + present: bool, + button_count: usize, + axis_count: usize, + + name: [max_name_len:0]u8, + buttons: [max_button_count]bool, + axes: [max_axis_count]f32, + + // 16 as it's the maximum number of joysticks supported by GLFW. + const max_joysticks = 16; + const max_name_len = 64; + const max_button_count = 32; + const max_axis_count = 16; +}; + +pub fn init( + core: *Core, + allocator: std.mem.Allocator, + frame: *Frequency, + input: *Frequency, + options: Options, +) !void { + _ = options; + var selector = [1]u8{0} ** 15; + const id = js.machCanvasInit(&selector[0]); + + core.* = Core{ + .allocator = allocator, + .frame = frame, + .input = input, + .id = id, + .input_state = .{}, + .joysticks = std.mem.zeroes([JoystickData.max_joysticks]JoystickData), + }; + + // TODO(wasm): wgpu support + mach_core.adapter = undefined; + mach_core.device = undefined; + mach_core.queue = undefined; + mach_core.swap_chain = undefined; + mach_core.descriptor = undefined; + + try core.frame.start(); + try core.input.start(); +} + +pub fn deinit(self: *Core) void { + js.machCanvasDeinit(self.id); +} + +pub inline fn update(self: *Core, app: anytype) !bool { + self.frame.tick(); + self.input.tick(); + if (try app.update()) return true; + if (@hasDecl(std.meta.Child(@TypeOf(app)), "updateMainThread")) { + if (app.updateMainThread() catch |err| @panic(@errorName(err))) { + return true; + } + } + return false; +} + +pub inline fn pollEvents(self: *Core) EventIterator { + return EventIterator{ + .core = self, + }; +} + +pub fn setTitle(self: *Core, title: [:0]const u8) void { + js.machCanvasSetTitle(self.id, title.ptr, title.len); +} + +pub fn setDisplayMode(self: *Core, _mode: DisplayMode) void { + var mode = _mode; + if (mode == .borderless) { + // borderless fullscreen window has no meaning in web + mode = .fullscreen; + } + js.machCanvasSetDisplayMode(self.id, @intFromEnum(mode)); +} + +pub fn displayMode(self: *Core) DisplayMode { + return @as(DisplayMode, @enumFromInt(js.machDisplayMode(self.id))); +} + +pub fn setBorder(self: *Core, value: bool) void { + _ = self; + _ = value; +} + +pub fn border(self: *Core) bool { + _ = self; + return false; +} + +pub fn setHeadless(self: *Core, value: bool) void { + _ = self; + _ = value; +} + +pub fn headless(self: *Core) bool { + _ = self; + return false; +} + +pub fn setVSync(self: *Core, mode: VSyncMode) void { + _ = mode; + self.frame.target = 0; +} + +// TODO(wasm): https://github.com/gpuweb/gpuweb/issues/1224 +pub fn vsync(self: *Core) VSyncMode { + _ = self; + return .double; +} + +pub fn setSize(self: *Core, value: Size) void { + js.machCanvasSetSize(self.id, value.width, value.height); +} + +pub fn size(self: *Core) Size { + return .{ + .width = js.machCanvasWidth(self.id), + .height = js.machCanvasHeight(self.id), + }; +} + +pub fn setSizeLimit(self: *Core, limit: SizeLimit) void { + js.machCanvasSetSizeLimit( + self.id, + if (limit.min.width) |val| @as(i32, @intCast(val)) else -1, + if (limit.min.height) |val| @as(i32, @intCast(val)) else -1, + if (limit.max.width) |val| @as(i32, @intCast(val)) else -1, + if (limit.max.height) |val| @as(i32, @intCast(val)) else -1, + ); +} + +pub fn sizeLimit(self: *Core) SizeLimit { + return .{ + .min = .{ + .width = js.machCanvasMinWidth(self.id), + .height = js.machCanvasMinHeight(self.id), + }, + .max = .{ + .width = js.machCanvasMaxWidth(self.id), + .height = js.machCanvasMaxHeight(self.id), + }, + }; +} + +pub fn setCursorMode(self: *Core, mode: CursorMode) void { + js.machSetCursorMode(self.id, @intFromEnum(mode)); +} + +pub fn cursorMode(self: *Core) CursorMode { + return @as(CursorMode, @enumFromInt(js.machCursorMode(self.id))); +} + +pub fn setCursorShape(self: *Core, shape: CursorShape) void { + js.machSetCursorShape(self.id, @intFromEnum(shape)); +} + +pub fn cursorShape(self: *Core) CursorShape { + return @as(CursorShape, @enumFromInt(js.machCursorShape(self.id))); +} + +pub fn joystickPresent(core: *Core, joystick: Joystick) bool { + const idx: u8 = @intFromEnum(joystick); + return core.joysticks[idx].present; +} + +pub fn joystickName(core: *Core, joystick: Joystick) ?[:0]const u8 { + const idx: u8 = @intFromEnum(joystick); + var data = &core.joysticks[idx]; + if (!data.present) return null; + + return std.mem.span(&data.name); +} + +pub fn joystickButtons(core: *Core, joystick: Joystick) ?[]const bool { + const idx: u8 = @intFromEnum(joystick); + var data = &core.joysticks[idx]; + if (!data.present) return null; + + js.machJoystickButtons(idx, &data.buttons, JoystickData.max_button_count); + return data.buttons[0..data.button_count]; +} + +pub fn joystickAxes(core: *Core, joystick: Joystick) ?[]const f32 { + const idx: u8 = @intFromEnum(joystick); + var data = &core.joysticks[idx]; + if (!data.present) return null; + + js.machJoystickAxes(idx, &data.axes, JoystickData.max_axis_count); + return data.buttons[0..data.button_count]; +} + +pub fn keyPressed(self: *Core, key: Key) bool { + return self.input_state.isKeyPressed(key); +} + +pub fn keyReleased(self: *Core, key: Key) bool { + return self.input_state.isKeyReleased(key); +} + +pub fn mousePressed(self: *Core, button: MouseButton) bool { + return self.input_state.isMouseButtonPressed(button); +} + +pub fn mouseReleased(self: *Core, button: MouseButton) bool { + return self.input_state.isMouseButtonReleased(button); +} + +pub fn mousePosition(self: *Core) Core.Position { + return self.input_state.mouse_position; +} + +pub inline fn adapter(_: *Core) *gpu.Adapter { + unreachable; +} + +pub inline fn device(_: *Core) *gpu.Device { + unreachable; +} + +pub inline fn swapChain(_: *Core) *gpu.SwapChain { + unreachable; +} + +pub inline fn descriptor(self: *Core) gpu.SwapChain.Descriptor { + return .{ + .label = "main swap chain", + .usage = .{ .render_attachment = true }, + .format = .bgra8_unorm, // TODO(wasm): is this correct? + .width = js.machCanvasFramebufferWidth(self.id), + .height = js.machCanvasFramebufferHeight(self.id), + .present_mode = .fifo, // TODO(wasm): https://github.com/gpuweb/gpuweb/issues/1224 + }; +} + +pub inline fn outOfMemory(self: *Core) bool { + _ = self; + return false; +} + +pub inline fn wakeMainThread(self: *Core) void { + _ = self; +} + +fn toMachButton(button: i32) MouseButton { + return switch (button) { + 0 => .left, + 1 => .middle, + 2 => .right, + 3 => .four, + 4 => .five, + else => unreachable, + }; +} diff --git a/src/core/platform/wasm/Timer.zig b/src/core/platform/wasm/Timer.zig new file mode 100644 index 00000000..ee833a51 --- /dev/null +++ b/src/core/platform/wasm/Timer.zig @@ -0,0 +1,25 @@ +const std = @import("std"); +const js = @import("js.zig"); + +pub const Timer = @This(); + +initial: f64 = undefined, + +pub fn start() !Timer { + return Timer{ .initial = js.machPerfNow() }; +} + +pub fn read(timer: *Timer) u64 { + return @intFromFloat((js.machPerfNow() - timer.initial) * std.time.ns_per_ms); +} + +pub fn reset(timer: *Timer) void { + timer.initial = js.machPerfNow(); +} + +pub fn lap(timer: *Timer) u64 { + const now = js.machPerfNow(); + const initial = timer.initial; + timer.initial = now; + return @as(u64, @intFromFloat(now - initial)) * std.time.ns_per_ms; +} diff --git a/src/core/platform/wasm/entrypoint.zig b/src/core/platform/wasm/entrypoint.zig new file mode 100644 index 00000000..23c86a69 --- /dev/null +++ b/src/core/platform/wasm/entrypoint.zig @@ -0,0 +1,42 @@ +// Check that the user's app matches the required interface. +comptime { + if (!@import("builtin").is_test) @import("mach").core.AppInterface(@import("app")); +} + +// Forward "app" declarations into our namespace, such that @import("root").foo works as expected. +pub usingnamespace @import("app"); +const App = @import("app").App; + +const std = @import("std"); +const core = @import("mach").core; +const gpu = core.gpu; + +pub const GPUInterface = gpu.StubInterface; + +var app: App = undefined; +export fn wasmInit() void { + App.init(&app) catch |err| @panic(@errorName(err)); +} + +export fn wasmUpdate() bool { + if (core.update(&app) catch |err| @panic(@errorName(err))) { + return true; + } + return false; +} + +export fn wasmDeinit() void { + app.deinit(); +} + +// Define std_options.logFn if the user did not in their "app" main.zig +pub usingnamespace if (@hasDecl(App, "std_options")) struct {} else struct { + pub const std_options = struct { + pub const logFn = core.defaultLog; + }; +}; + +// Define panic() if the user did not in their "app" main.zig +pub usingnamespace if (@hasDecl(App, "panic")) struct {} else struct { + pub const panic = core.defaultPanic; +}; diff --git a/src/core/platform/wasm/js.zig b/src/core/platform/wasm/js.zig new file mode 100644 index 00000000..68777be1 --- /dev/null +++ b/src/core/platform/wasm/js.zig @@ -0,0 +1,42 @@ +pub const CanvasId = u32; + +pub extern "mach" fn machLogWrite(str: [*]const u8, len: u32) void; +pub extern "mach" fn machLogFlush() void; +pub extern "mach" fn machPanic(str: [*]const u8, len: u32) void; + +pub extern "mach" fn machCanvasInit(selector_id: *u8) CanvasId; +pub extern "mach" fn machCanvasDeinit(canvas: CanvasId) void; +pub extern "mach" fn machCanvasFramebufferWidth(canvas: CanvasId) u32; +pub extern "mach" fn machCanvasFramebufferHeight(canvas: CanvasId) u32; +pub extern "mach" fn machCanvasSetTitle(canvas: CanvasId, title: [*]const u8, len: u32) void; +pub extern "mach" fn machCanvasSetDisplayMode(canvas: CanvasId, mode: u32) void; +pub extern "mach" fn machCanvasDisplayMode(canvas: CanvasId) u32; +pub extern "mach" fn machCanvasSetBorder(canvas: CanvasId, value: bool) void; +pub extern "mach" fn machCanvasBorder(canvas: CanvasId) bool; +pub extern "mach" fn machCanvasSetHeadless(canvas: CanvasId, value: bool) void; +pub extern "mach" fn machCanvasHeadless(canvas: CanvasId) bool; +pub extern "mach" fn machCanvasSetVsync(canvas: CanvasId, mode: u32) void; +pub extern "mach" fn machCanvasVsync(canvas: CanvasId) u32; +pub extern "mach" fn machCanvasSetSize(canvas: CanvasId, width: u32, height: u32) void; +pub extern "mach" fn machCanvasWidth(canvas: CanvasId) u32; +pub extern "mach" fn machCanvasHeight(canvas: CanvasId) u32; +pub extern "mach" fn machCanvasSetSizeLimit(canvas: CanvasId, min_width: i32, min_height: i32, max_width: i32, max_height: i32) void; +pub extern "mach" fn machCanvasMinWidth(canvas: CanvasId) u32; +pub extern "mach" fn machCanvasMinHeight(canvas: CanvasId) u32; +pub extern "mach" fn machCanvasMaxWidth(canvas: CanvasId) u32; +pub extern "mach" fn machCanvasMaxHeight(canvas: CanvasId) u32; +pub extern "mach" fn machSetCursorMode(canvas: CanvasId, mode: u32) void; +pub extern "mach" fn machCursorMode(canvas: CanvasId) u32; +pub extern "mach" fn machSetCursorShape(canvas: CanvasId, shape: u32) void; +pub extern "mach" fn machCursorShape(canvas: CanvasId) u32; +pub extern "mach" fn machJoystickName(idx: u8, dest: [*:0]u8, dest_len: usize) void; +pub extern "mach" fn machJoystickButtons(idx: u8, dest: [*]u8, dest_len: usize) void; +pub extern "mach" fn machJoystickAxes(idx: u8, dest: [*]f32, dest_len: usize) void; + +pub extern "mach" fn machShouldClose() bool; +pub extern "mach" fn machHasEvent() bool; +pub extern "mach" fn machEventShift() i32; +pub extern "mach" fn machEventShiftFloat() f64; +pub extern "mach" fn machChangeShift() u32; + +pub extern "mach" fn machPerfNow() f64; diff --git a/src/core/platform/wasm/mach.js b/src/core/platform/wasm/mach.js new file mode 100644 index 00000000..33f88c49 --- /dev/null +++ b/src/core/platform/wasm/mach.js @@ -0,0 +1,569 @@ +const text_decoder = new TextDecoder(); +const text_encoder = new TextEncoder(); + +const mach = { + canvases: [], + wasm: undefined, + observer: undefined, + events: [], + changes: [], + log_buf: "", + + init(wasm) { + mach.wasm = wasm; + mach.observer = new MutationObserver((mutables) => { + mutables.forEach((mutable) => { + mach.canvases.forEach((canvas) => { + if (mutable.target == canvas) { + if (mutable.attributeName === "width" || + mutable.attributeName === "height" || + mutable.attributeName === "style") { + mutable.target.dispatchEvent(new Event("mach-canvas-resize")); + } + } + }); + }) + }) + }, + + getString(str, len) { + const memory = mach.wasm.exports.memory.buffer; + return text_decoder.decode(new Uint8Array(memory, str, len)); + }, + + setString(str, buf) { + const memory = mach.wasm.exports.memory.buffer; + const strbuf = text_encoder.encode(str); + const outbuf = new Uint8Array(memory, buf, strbuf.length); + for (let i = 0; i < strbuf.length; i += 1) { + outbuf[i] = strbuf[i]; + } + }, + + machLogWrite(str, len) { + mach.log_buf += mach.getString(str, len); + }, + + machLogFlush() { + console.log(mach.log_buf); + mach.log_buf = ""; + }, + + machPanic(str, len) { + throw Error(mach.getString(str, len)); + }, + + machCanvasInit(id) { + let canvas = document.createElement("canvas"); + canvas.id = "#mach-canvas-" + mach.canvases.length; + canvas.style.border = "1px solid"; + canvas.style.position = "absolute"; + canvas.style.display = "block"; + canvas.tabIndex = 1; + + mach.observer.observe(canvas, { attributes: true }); + + mach.setString(canvas.id, id); + + canvas.addEventListener("contextmenu", (ev) => ev.preventDefault()); + + canvas.addEventListener("keydown", (ev) => { + if (ev.repeat) { + mach.events.push(...[EventCode.key_repeat, convertKeyCode(ev.code)]); + } else { + mach.events.push(...[EventCode.key_press, convertKeyCode(ev.code)]); + } + }); + + canvas.addEventListener("keyup", (ev) => { + mach.events.push(...[EventCode.key_release, convertKeyCode(ev.code)]); + }); + + canvas.addEventListener("mousemove", (ev) => { + mach.events.push(...[EventCode.mouse_motion, ev.clientX, ev.clientY]); + }); + + canvas.addEventListener("mousedown", (ev) => { + mach.events.push(...[EventCode.mouse_press, ev.button]); + }); + + canvas.addEventListener("mouseup", (ev) => { + mach.events.push(...[EventCode.mouse_release, ev.button]); + }); + + canvas.addEventListener("wheel", (ev) => { + mach.events.push(...[EventCode.mouse_scroll, ev.deltaX, ev.deltaY]); + }); + + canvas.addEventListener("gamepadconnected", (ev) => { + mach.events.push(...[ + EventCode.joystick_connected, ev.gamepad.index, + ev.gamepad.buttons.length, ev.gamepad.axes.length, + ]); + }); + + canvas.addEventListener("gamepaddisconnected", (ev) => { + mach.events.push(...[EventCode.joystick_disconnected, ev.gamepad.index]); + }); + + canvas.addEventListener("mach-canvas-resize", (ev) => { + const cv_index = mach.canvases.findIndex((el) => el === ev.currentTarget); + const cv = mach.canvases[cv_index]; + mach.events.push(...[EventCode.framebuffer_resize, cv.width, cv.height, window.devicePixelRatio]); + }); + + canvas.addEventListener("focus", (ev) => { + mach.events.push(...[EventCode.focus_gained]); + }); + + canvas.addEventListener("blur", (ev) => { + mach.events.push(...[EventCode.focus_lost]); + }); + + document.body.appendChild(canvas); + return mach.canvases.push(canvas) - 1; + }, + + machCanvasDeinit(canvas) { + if (mach.canvases[canvas] != undefined) { + mach.canvases.splice(canvas, 1); + } + }, + + machCanvasFramebufferWidth(canvas) { + const cv = mach.canvases[canvas]; + return cv.width; + }, + + machCanvasFramebufferHeight(canvas) { + const cv = mach.canvases[canvas]; + return cv.height; + }, + + machCanvasSetTitle(canvas, title, len) { + // TODO(wasm) + }, + + machCanvasSetDisplayMode(canvas, mode) { + const cv = mach.canvases[canvas]; + switch (mode) { + case DisplayMode.windowed: + document.exitFullscreen(); + break; + case DisplayMode.fullscreen: + cv.requestFullscreen(); + break; + } + }, + + machCanvasDisplayMode(canvas) { + if (mach.canvases[canvas].fullscreenElement == null) { + return DisplayMode.windowed; + } else { + return DisplayMode.fullscreen; + } + }, + + machCanvasSetBorder(canvas, value) { + // TODO(wasm) + }, + + machCanvasBorder(canvas) { + // TODO(wasm) + }, + + machCanvasSetHeadless(canvas, value) { + // TODO(wasm) + }, + + machCanvasHeadless(canvas) { + // TODO(wasm) + }, + + machCanvasSetVSync(canvas, mode) { + // TODO(wasm) + }, + + machCanvasVSync(canvas) { + // TODO(wasm) + }, + + machCanvasSetSize(canvas, width, height) { + const cv = mach.canvases[canvas]; + if (width > 0 && height > 0) { + cv.style.width = width + "px"; + cv.style.height = height + "px"; + cv.width = Math.floor(width * window.devicePixelRatio); + cv.height = Math.floor(height * window.devicePixelRatio); + } + }, + + machCanvasWidth(canvas) { + const cv = mach.canvases[canvas]; + return cv.width / window.devicePixelRatio; + }, + + machCanvasHeight(canvas) { + const cv = mach.canvases[canvas]; + return cv.height / window.devicePixelRatio; + }, + + machCanvasSetSizeLimit(canvas, min_width, min_height, max_width, max_height) { + const cv = mach.canvases[canvas]; + if (min_width == -1) { + cv.style.minWidth = "inherit" + } else { + cv.style.minWidth = min_width + "px"; + } + if (min_width == -1) { + cv.style.minHeight = "inherit" + } else { + cv.style.minHeight = min_height + "px"; + } + if (min_width == -1) { + cv.style.maxWidth = "inherit" + } else { + cv.style.maxWidth = max_width + "px"; + } + if (min_width == -1) { + cv.style.maxHeight = "inherit" + } else { + cv.style.maxHeight = max_height + "px"; + } + }, + + machCanvasMinWidth(canvas) { + const cv = mach.canvases[canvas]; + return cv.style.minWidth; + }, + + machCanvasMinHeight(canvas) { + const cv = mach.canvases[canvas]; + return cv.style.minHeight; + }, + + machCanvasMaxWidth(canvas) { + const cv = mach.canvases[canvas]; + return cv.style.maxWidth; + }, + + machCanvasMaxHeight(canvas) { + const cv = mach.canvases[canvas]; + return cv.style.maxHeight; + }, + + machSetCursorMode(canvas, mode) { + const cv = mach.canvases[canvas]; + switch (mode) { + case CursorMode.normal: + cv.style.cursor = 'default'; + break; + case CursorMode.hidden: + cv.style.cursor = 'none'; + break; + case CursorMode.hidden: + cv.style.cursor = 'none'; + break; + } + }, + + machCursorMode(canvas) { + switch (mach.canvases[canvas].style.cursor) { + case 'none': return CursorMode.hidden; + default: return CursorMode.normal; + } + }, + + machSetCursorShape(canvas, shape) { + const cv = mach.canvases[canvas]; + switch (shape) { + case CursorShape.arrow: + cv.style.cursor = 'default'; + break; + case CursorShape.ibeam: + cv.style.cursor = 'text'; + break; + case CursorShape.crosshair: + cv.style.cursor = 'crosshair'; + break; + case CursorShape.pointing_hand: + cv.style.cursor = 'pointer'; + break; + case CursorShape.resize_ew: + cv.style.cursor = 'ew-resize'; + break; + case CursorShape.resize_ns: + cv.style.cursor = 'ns-resize'; + break; + case CursorShape.resize_nwse: + cv.style.cursor = 'nwse-resize'; + break; + case CursorShape.resize_nesw: + cv.style.cursor = 'nesw-resize'; + break; + case CursorShape.resize_all: + cv.style.cursor = 'move'; + break; + case CursorShape.not_allowed: + cv.style.cursor = 'not-allowed'; + break; + } + }, + + machCursorShape(canvas) { + switch (mach.canvases[canvas].style.cursor) { + case 'default': return CursorShape.arrow; + case 'text': return CursorShape.ibeam; + case 'crosshair': return CursorShape.crosshair; + case 'pointer': return CursorShape.pointing_hand; + case 'ew-resize': return CursorShape.resize_ew; + case 'ns-resize': return CursorShape.resize_ns; + case 'nwse-resize': return CursorShape.resize_nwse; + case 'nesw-resize': return CursorShape.resize_nesw; + case 'move': return CursorShape.resize_all; + case 'not-allowed': return CursorShape.not_allowed; + } + }, + + machJoystickName(idx, dest, dest_len) { + const gamepads = navigator.getGamepads(); + + if( idx < 0 || idx > gamepads.length ) + return; + + if( gamepads[idx] === null || gamepads[idx] === undefined ) + return; + + const name = gamepads[idx].id; + const strbuf = text_encoder.encode(name).slice(0, dest_len); + const dstbuf = new Uint8Array(mach.wasm.exports.memory.buffer, dest, dest_len+1); + dstbuf.set(strbuf); + dstbuf[dest_len] = 0; + }, + + machJoystickButtons(idx, dest, dest_len) { + const gamepads = navigator.getGamepads(); + + if( idx < 0 || idx > gamepads.length ) + return; + + if( gamepads[idx] === null || gamepads[idx] === undefined ) + return; + + const buttons = gamepads[idx].buttons.map(x => x.pressed ? 1 : 0); + const count = Math.min(dest_len, buttons.length); + const dstbuf = new Uint8Array(mach.wasm.exports.memory.buffer, dest, count); + dstbuf.set(buttons); + }, + + machJoystickAxes(idx, dest, dest_len) { + const gamepads = navigator.getGamepads(); + + if( idx < 0 || idx > gamepads.length ) + return; + + if( gamepads[idx] === null || gamepads[idx] === undefined ) + return; + + const axes = gamepads[idx].axes; + const count = Math.min(dest_len, axes.length); + const dstbuf = new Float32Array(mach.wasm.exports.memory.buffer, dest, count); + dstbuf.set(axes); + }, + + machHasEvent() { + return mach.events.length > 0; + }, + + machEventShift() { + if (mach.machHasEvent()) + return mach.events.shift(); + + return -1; + }, + + machEventShiftFloat() { + return mach.machEventShift(); + }, + + machPerfNow() { + return performance.now(); + }, +}; + +function convertKeyCode(code) { + const k = Key[code]; + if (k != undefined) + return k; + return 118; // Unknown +} + +const EventCode = { + key_press: 0, + key_repeat: 1, + key_release: 2, + char_input: 3, + mouse_motion: 4, + mouse_press: 5, + mouse_release: 6, + mouse_scroll: 7, + joystick_connected: 8, + joystick_disconnected: 9, + framebuffer_resize: 10, + focus_gained: 11, + focus_lost: 12, + close: 13, +}; + +const Key = { + KeyA: 0, + KeyB: 1, + KeyC: 2, + KeyD: 3, + KeyE: 4, + KeyF: 5, + KeyG: 6, + KeyH: 7, + KeyI: 8, + KeyJ: 9, + KeyK: 10, + KeyL: 11, + KeyM: 12, + KeyN: 13, + KeyO: 14, + KeyP: 15, + KeyQ: 16, + KeyR: 17, + KeyS: 18, + KeyT: 19, + KeyU: 20, + KeyV: 21, + KeyW: 22, + KeyX: 23, + KeyY: 24, + KeyZ: 25, + + Digit0: 26, + Digit1: 27, + Digit2: 28, + Digit3: 29, + Digit4: 30, + Digit5: 31, + Digit6: 32, + Digit7: 33, + Digit8: 34, + Digit9: 35, + + F1: 36, + F2: 37, + F3: 38, + F4: 39, + F5: 40, + F6: 41, + F7: 42, + F8: 43, + F9: 44, + F10: 45, + F11: 46, + F12: 47, + F13: 48, + F14: 49, + F15: 50, + F16: 51, + F17: 52, + F18: 53, + F19: 54, + F20: 55, + F21: 56, + F22: 57, + F23: 58, + F24: 59, + F25: 60, + + NumpadDivide: 61, + NumpadMultiply: 62, + NumpadSubtract: 63, + NumpadAdd: 64, + Numpad0: 65, + Numpad1: 66, + Numpad2: 67, + Numpad3: 68, + Numpad4: 69, + Numpad5: 70, + Numpad6: 71, + Numpad7: 72, + Numpad8: 73, + Numpad9: 74, + NumpadDecimal: 75, + NumpadEqual: 76, + NumpadEnter: 77, + + Enter: 78, + Escape: 79, + Tab: 80, + ShiftLeft: 81, + ShiftRight: 82, + ControlLeft: 83, + ControlRight: 84, + AltLeft: 85, + AltRight: 86, + OSLeft: 87, + MetaLeft: 87, + OSRight: 88, + MetaRight: 88, + ContextMenu: 89, + NumLock: 90, + CapsLock: 91, + PrintScreen: 92, + ScrollLock: 93, + Pause: 94, + Delete: 95, + Home: 96, + End: 97, + PageUp: 98, + PageDown: 99, + Insert: 100, + ArrowLeft: 101, + ArrowRight: 102, + ArrowUp: 103, + ArrowDown: 104, + Backspace: 105, + Space: 106, + Minus: 107, + Equal: 108, + BracketLeft: 109, + BracketRight: 110, + Backslash: 111, + Semicolon: 112, + Quote: 113, + Comma: 114, + Period: 115, + Slash: 116, + Backquote: 117, +}; + +const DisplayMode = { + windowed: 0, + fullscreen: 1, +}; + +const CursorMode = { + normal: 0, + hidden: 1, + disabled: 2, +}; + +const CursorShape = { + arrow: 0, + ibeam: 1, + crosshair: 2, + pointing_hand: 3, + resize_ew: 4, + resize_ns: 5, + resize_nwse: 6, + resize_nesw: 7, + resize_all: 8, + not_allowed: 9, +}; + +export { mach }; diff --git a/src/core/platform/wayland.zig b/src/core/platform/wayland.zig new file mode 100644 index 00000000..284852cf --- /dev/null +++ b/src/core/platform/wayland.zig @@ -0,0 +1,4 @@ +const std = @import("std"); + +pub const Core = @import("wayland/Core.zig"); +pub const Timer = std.time.Timer; diff --git a/src/core/platform/wayland/Core.zig b/src/core/platform/wayland/Core.zig new file mode 100644 index 00000000..774b6a1e --- /dev/null +++ b/src/core/platform/wayland/Core.zig @@ -0,0 +1,1459 @@ +const std = @import("std"); +const mach_core = @import("../../main.zig"); +const gpu = mach_core.gpu; +const Options = @import("../../main.zig").Options; +const Event = @import("../../main.zig").Event; +const KeyEvent = @import("../../main.zig").KeyEvent; +const MouseButtonEvent = @import("../../main.zig").MouseButtonEvent; +const MouseButton = @import("../../main.zig").MouseButton; +const Size = @import("../../main.zig").Size; +const DisplayMode = @import("../../main.zig").DisplayMode; +const SizeLimit = @import("../../main.zig").SizeLimit; +const CursorShape = @import("../../main.zig").CursorShape; +const VSyncMode = @import("../../main.zig").VSyncMode; +const CursorMode = @import("../../main.zig").CursorMode; +const Key = @import("../../main.zig").Key; +const KeyMods = @import("../../main.zig").KeyMods; +const Joystick = @import("../../main.zig").Joystick; +const InputState = @import("../../InputState.zig"); +const Frequency = @import("../../Frequency.zig"); +const RequestAdapterResponse = @import("../common.zig").RequestAdapterResponse; +const printUnhandledErrorCallback = @import("../common.zig").printUnhandledErrorCallback; +const detectBackendType = @import("../common.zig").detectBackendType; +const wantGamemode = @import("../common.zig").wantGamemode; +const initLinuxGamemode = @import("../common.zig").initLinuxGamemode; +const deinitLinuxGamemode = @import("../common.zig").deinitLinuxGamemode; +const requestAdapterCallback = @import("../common.zig").requestAdapterCallback; +const Unicode = @import("../x11/unicode.zig"); + +const log = std.log.scoped(.mach); + +pub const c = @cImport({ + @cInclude("wayland-client-protocol.h"); + @cInclude("wayland-xdg-shell-client-protocol.h"); + @cInclude("wayland-xdg-decoration-client-protocol.h"); + @cInclude("wayland-viewporter-client-protocol.h"); + @cInclude("wayland-relative-pointer-unstable-v1-client-protocol.h"); + @cInclude("wayland-pointer-constraints-unstable-v1-client-protocol.h"); + @cInclude("wayland-idle-inhibit-unstable-v1-client-protocol.h"); + @cInclude("xkbcommon/xkbcommon.h"); + @cInclude("xkbcommon/xkbcommon-compose.h"); + @cInclude("linux/input-event-codes.h"); +}); + +var libwaylandclient: LibWaylandClient = undefined; + +export fn wl_proxy_add_listener(proxy: ?*c.struct_wl_proxy, implementation: [*c]?*const fn () callconv(.C) void, data: ?*anyopaque) c_int { + return @call(.always_tail, libwaylandclient.wl_proxy_add_listener, .{ proxy, implementation, data }); +} + +export fn wl_proxy_get_version(proxy: ?*c.struct_wl_proxy) u32 { + return @call(.always_tail, libwaylandclient.wl_proxy_get_version, .{proxy}); +} + +export fn wl_proxy_marshal_flags(proxy: ?*c.struct_wl_proxy, opcode: u32, interface: [*c]const c.struct_wl_interface, version: u32, flags: u32, ...) ?*c.struct_wl_proxy { + var arg_list: std.builtin.VaList = @cVaStart(); + defer @cVaEnd(&arg_list); + + return @call(.always_tail, libwaylandclient.wl_proxy_marshal_flags, .{ proxy, opcode, interface, version, flags, arg_list }); +} + +export fn wl_proxy_destroy(proxy: ?*c.struct_wl_proxy) void { + return @call(.always_tail, libwaylandclient.wl_proxy_destroy, .{proxy}); +} + +const LibXkbCommon = struct { + handle: std.DynLib, + + xkb_context_new: *const @TypeOf(c.xkb_context_new), + xkb_keymap_new_from_string: *const @TypeOf(c.xkb_keymap_new_from_string), + xkb_state_new: *const @TypeOf(c.xkb_state_new), + xkb_keymap_unref: *const @TypeOf(c.xkb_keymap_unref), + xkb_state_unref: *const @TypeOf(c.xkb_state_unref), + xkb_compose_table_new_from_locale: *const @TypeOf(c.xkb_compose_table_new_from_locale), + xkb_compose_state_new: *const @TypeOf(c.xkb_compose_state_new), + xkb_compose_table_unref: *const @TypeOf(c.xkb_compose_table_unref), + xkb_keymap_mod_get_index: *const @TypeOf(c.xkb_keymap_mod_get_index), + xkb_state_update_mask: *const @TypeOf(c.xkb_state_update_mask), + xkb_state_mod_index_is_active: *const @TypeOf(c.xkb_state_mod_index_is_active), + xkb_state_key_get_syms: *const @TypeOf(c.xkb_state_key_get_syms), + xkb_compose_state_feed: *const @TypeOf(c.xkb_compose_state_feed), + xkb_compose_state_get_status: *const @TypeOf(c.xkb_compose_state_get_status), + xkb_compose_state_get_one_sym: *const @TypeOf(c.xkb_compose_state_get_one_sym), + + pub fn load() !LibXkbCommon { + var lib: LibXkbCommon = undefined; + lib.handle = std.DynLib.openZ("libxkbcommon.so.0") catch return error.LibraryNotFound; + inline for (@typeInfo(LibXkbCommon).Struct.fields[1..]) |field| { + const name = std.fmt.comptimePrint("{s}\x00", .{field.name}); + const name_z: [:0]const u8 = @ptrCast(name[0 .. name.len - 1]); + @field(lib, field.name) = lib.handle.lookup(field.type, name_z) orelse { + log.err("Symbol lookup failed for {s}", .{name}); + return error.SymbolLookup; + }; + } + return lib; + } +}; + +const LibWaylandClient = struct { + handle: std.DynLib, + + wl_display_connect: *const @TypeOf(c.wl_display_connect), + wl_proxy_add_listener: *const @TypeOf(c.wl_proxy_add_listener), + wl_proxy_get_version: *const @TypeOf(c.wl_proxy_get_version), + wl_proxy_marshal_flags: *const @TypeOf(c.wl_proxy_marshal_flags), + wl_proxy_set_tag: *const @TypeOf(c.wl_proxy_set_tag), + wl_proxy_destroy: *const @TypeOf(c.wl_proxy_destroy), + wl_display_roundtrip: *const @TypeOf(c.wl_display_roundtrip), + wl_display_dispatch: *const @TypeOf(c.wl_display_dispatch), + wl_display_flush: *const @TypeOf(c.wl_display_flush), + wl_display_get_fd: *const @TypeOf(c.wl_display_get_fd), + + //Interfaces + wl_compositor_interface: *@TypeOf(c.wl_compositor_interface), + wl_subcompositor_interface: *@TypeOf(c.wl_subcompositor_interface), + wl_shm_interface: *@TypeOf(c.wl_subcompositor_interface), + wl_data_device_manager_interface: *@TypeOf(c.wl_data_device_manager_interface), + + wl_buffer_interface: *@TypeOf(c.wl_buffer_interface), + wl_callback_interface: *@TypeOf(c.wl_callback_interface), + wl_data_device_interface: *@TypeOf(c.wl_data_device_interface), + wl_data_offer_interface: *@TypeOf(c.wl_data_offer_interface), + wl_data_source_interface: *@TypeOf(c.wl_data_source_interface), + wl_keyboard_interface: *@TypeOf(c.wl_keyboard_interface), + wl_output_interface: *@TypeOf(c.wl_output_interface), + wl_pointer_interface: *@TypeOf(c.wl_pointer_interface), + wl_region_interface: *@TypeOf(c.wl_region_interface), + wl_registry_interface: *@TypeOf(c.wl_registry_interface), + wl_seat_interface: *@TypeOf(c.wl_seat_interface), + wl_shell_surface_interface: *@TypeOf(c.wl_shell_surface_interface), + wl_shm_pool_interface: *@TypeOf(c.wl_shm_pool_interface), + wl_subsurface_interface: *@TypeOf(c.wl_subsurface_interface), + wl_surface_interface: *@TypeOf(c.wl_surface_interface), + wl_touch_interface: *@TypeOf(c.wl_touch_interface), + + pub extern const xdg_wm_base_interface: @TypeOf(c.xdg_wm_base_interface); + pub extern const zxdg_decoration_manager_v1_interface: @TypeOf(c.zxdg_decoration_manager_v1_interface); + + pub fn load() !LibWaylandClient { + var lib: LibWaylandClient = undefined; + lib.handle = std.DynLib.openZ("libwayland-client.so.0") catch return error.LibraryNotFound; + inline for (@typeInfo(LibWaylandClient).Struct.fields[1..]) |field| { + const name = std.fmt.comptimePrint("{s}\x00", .{field.name}); + const name_z: [:0]const u8 = @ptrCast(name[0 .. name.len - 1]); + @field(lib, field.name) = lib.handle.lookup(field.type, name_z) orelse { + log.err("Symbol lookup failed for {s}", .{name}); + return error.SymbolLookup; + }; + } + return lib; + } +}; + +const EventQueue = std.fifo.LinearFifo(Event, .Dynamic); + +pub const EventIterator = struct { + events_mu: *std.Thread.RwLock, + queue: *EventQueue, + + pub inline fn next(self: *EventIterator) ?Event { + self.events_mu.lockShared(); + defer self.events_mu.unlockShared(); + return self.queue.readItem(); + } +}; + +const Interfaces = struct { + wl_compositor: ?*c.wl_compositor = null, + wl_subcompositor: ?*c.wl_subcompositor = null, + wl_shm: ?*c.wl_shm = null, + wl_output: ?*c.wl_output = null, + wl_seat: ?*c.wl_seat = null, + wl_data_device_manager: ?*c.wl_data_device_manager = null, + xdg_wm_base: ?*c.xdg_wm_base = null, + zxdg_decoration_manager_v1: ?*c.zxdg_decoration_manager_v1 = null, + // wp_viewporter: *c.wp_viewporter, + // zwp_relative_pointer_manager_v1: *c.zwp_relative_pointer_manager_v1, + // zwp_pointer_constraints_v1: *c.zwp_pointer_constraints_v1, + // zwp_idle_inhibit_manager_v1: *c.zwp_idle_inhibit_manager_v1, + // xdg_activation_v1: *c.xdg_activation_v1, +}; + +fn Changable(comptime T: type, comptime uses_allocator: bool) type { + return struct { + current: T, + last: if (uses_allocator) ?T else void, + allocator: if (uses_allocator) std.mem.Allocator else void, + changed: bool = false, + + const Self = @This(); + + ///Initialize with a default value + pub fn init(value: T, allocator: if (uses_allocator) std.mem.Allocator else void) !Self { + if (uses_allocator) { + return .{ + .allocator = allocator, + .last = null, + .current = try allocator.dupeZ(std.meta.Child(T), value), + }; + } else { + return .{ + .allocator = {}, + .last = {}, + .current = value, + }; + } + } + + /// Set a new value for the changable + pub fn set(self: *Self, value: T) !void { + if (uses_allocator) { + //If we have a last value, free it + if (self.last) |last_value| { + self.allocator.free(last_value); + + self.last = null; + } + + self.last = self.current; + + self.current = try self.allocator.dupeZ(std.meta.Child(T), value); + } else { + self.current = value; + } + self.changed = true; + } + + /// Read the current value out, resetting the changed flag + pub fn read(self: *Self) ?T { + if (!self.changed) + return null; + + self.changed = false; + return self.current; + } + + /// Free's the last allocation and resets the `last` value + pub fn freeLast(self: *Self) void { + if (uses_allocator) { + if (self.last) |last_value| { + self.allocator.free(last_value); + } + + self.last = null; + } + } + + pub fn deinit(self: *Self) void { + if (uses_allocator) { + if (self.last) |last_value| { + self.allocator.free(last_value); + } + + self.allocator.free(self.current); + } + + self.* = undefined; + } + }; +} + +/// Global state passed to things as the user data parameter, anything that needs to be accessed by callbacks should be in here. +const GlobalState = struct { + //xkb + libxkbcommon: LibXkbCommon, + xkb_context: ?*c.xkb_context, + keymap: ?*c.xkb_keymap, + xkb_state: ?*c.xkb_state, + compose_state: ?*c.xkb_compose_state, + + control_index: c.xkb_mod_index_t, + alt_index: c.xkb_mod_index_t, + shift_index: c.xkb_mod_index_t, + super_index: c.xkb_mod_index_t, + caps_lock_index: c.xkb_mod_index_t, + num_lock_index: c.xkb_mod_index_t, + + // Wayland objects/state + configured: bool, + interfaces: Interfaces, + surface: ?*c.struct_wl_surface, + + // Input/Event stuff + keyboard: ?*c.wl_keyboard = null, + pointer: ?*c.wl_pointer = null, + events_mu: std.Thread.RwLock = .{}, + events: EventQueue, + + input_state: InputState, + modifiers: KeyMods, + + //changables + state_mu: std.Thread.RwLock = .{}, + window_size_mu: std.Thread.RwLock = .{}, + window_size: Changable(Size, false), + swap_chain_update: std.Thread.ResetEvent = .{}, + + // Mutable fields; written by the App.update thread, read from any + swap_chain_mu: std.Thread.RwLock = .{}, + + fn pushEvent(self: *GlobalState, event: Event) void { + self.events_mu.lock(); + defer self.events_mu.unlock(); + + self.events.writeItem(event) catch @panic("TODO"); + } +}; + +pub const Core = @This(); + +gpu_device: *gpu.Device, +surface: *gpu.Surface, +swap_chain: *gpu.SwapChain, +swap_chain_desc: gpu.SwapChain.Descriptor, + +// Wayland objects/state +display: *c.struct_wl_display, +registry: *c.struct_wl_registry, +xdg_surface: *c.xdg_surface, +toplevel: *c.xdg_toplevel, +tag: [*]c_char, +decoration: *c.zxdg_toplevel_decoration_v1, +global_state: GlobalState, + +// internal tracking state +app_update_thread_started: bool = false, +done: std.Thread.ResetEvent = .{}, + +//timings +frame: *Frequency, +input: *Frequency, + +// changables +title: Changable([:0]const u8, true), +min_size: Changable(Size, false), +max_size: Changable(Size, false), + +fn registryHandleGlobal(user_data: *GlobalState, registry: ?*c.struct_wl_registry, name: u32, interface_ptr: [*:0]const u8, version: u32) callconv(.C) void { + const interface = std.mem.span(interface_ptr); + + log.debug("Got interface: {s}", .{interface}); + + if (std.mem.eql(u8, "wl_compositor", interface)) { + user_data.interfaces.wl_compositor = @ptrCast(c.wl_registry_bind( + registry, + name, + libwaylandclient.wl_compositor_interface, + @min(3, version), + ) orelse @panic("uh idk how to proceed")); + log.debug("Bound wl_compositor :)", .{}); + } else if (std.mem.eql(u8, "wl_subcompositor", interface)) { + user_data.interfaces.wl_subcompositor = @ptrCast(c.wl_registry_bind( + registry, + name, + libwaylandclient.wl_subcompositor_interface, + @min(3, version), + ) orelse @panic("uh idk how to proceed")); + log.debug("Bound wl_subcompositor :)", .{}); + } else if (std.mem.eql(u8, "wl_shm", interface)) { + user_data.interfaces.wl_shm = @ptrCast(c.wl_registry_bind( + registry, + name, + libwaylandclient.wl_shm_interface, + @min(3, version), + ) orelse @panic("uh idk how to proceed")); + log.debug("Bound wl_shm :)", .{}); + } else if (std.mem.eql(u8, "wl_output", interface)) { + user_data.interfaces.wl_output = @ptrCast(c.wl_registry_bind( + registry, + name, + libwaylandclient.wl_output_interface, + @min(3, version), + ) orelse @panic("uh idk how to proceed")); + log.debug("Bound wl_output :)", .{}); + // } else if (std.mem.eql(u8, "wl_data_device_manager", interface)) { + // user_data.interfaces.wl_data_device_manager = @ptrCast(user_data.libwaylandclient.wl_registry_bind( + // registry, + // name, + // user_data.libwaylandclient.wl_data_device_manager_interface, + // @min(3, version), + // ) orelse @panic("uh idk how to proceed")); + // log.debug("Bound wl_data_device_manager :)", .{}); + } else if (std.mem.eql(u8, "xdg_wm_base", interface)) { + user_data.interfaces.xdg_wm_base = @ptrCast(c.wl_registry_bind( + registry, + name, + &LibWaylandClient.xdg_wm_base_interface, + // &LibWaylandClient._glfw_xdg_wm_base_interface, + @min(3, version), + ) orelse @panic("uh idk how to proceed")); + log.debug("Bound xdg_wm_base :)", .{}); + + //TODO: handle return value + _ = c.xdg_wm_base_add_listener(user_data.interfaces.xdg_wm_base, &.{ .ping = @ptrCast(&wmBaseHandlePing) }, user_data); + } else if (std.mem.eql(u8, "zxdg_decoration_manager_v1", interface)) { + user_data.interfaces.zxdg_decoration_manager_v1 = @ptrCast(c.wl_registry_bind( + registry, + name, + &LibWaylandClient.zxdg_decoration_manager_v1_interface, + @min(3, version), + ) orelse @panic("uh idk how to proceed")); + log.debug("Bound zxdg_decoration_manager_v1 :)", .{}); + } else if (std.mem.eql(u8, "wl_seat", interface)) { + user_data.interfaces.wl_seat = @ptrCast(c.wl_registry_bind( + registry, + name, + libwaylandclient.wl_seat_interface, + @min(3, version), + ) orelse @panic("uh idk how to proceed")); + log.debug("Bound wl_seat :)", .{}); + + //TODO: handle return value + _ = c.wl_seat_add_listener(user_data.interfaces.wl_seat, &.{ + .capabilities = @ptrCast(&seatHandleCapabilities), + .name = @ptrCast(&seatHandleName), //ptrCast for the `[*:0]const u8` + }, user_data); + } +} + +fn seatHandleName(user_data: *GlobalState, seat: ?*c.struct_wl_seat, name_ptr: [*:0]const u8) callconv(.C) void { + _ = user_data; + const name = std.mem.span(name_ptr); + + log.info("seat {*} has name {s}", .{ seat, name }); +} + +fn seatHandleCapabilities(user_data: *GlobalState, seat: ?*c.struct_wl_seat, caps: c.wl_seat_capability) callconv(.C) void { + log.info("seat {*} has caps {d}", .{ seat, caps }); + + if ((caps & c.WL_SEAT_CAPABILITY_KEYBOARD) != 0) { + user_data.keyboard = c.wl_seat_get_keyboard(seat); + + //TODO: handle return value + _ = c.wl_keyboard_add_listener(user_data.keyboard, &.{ + .keymap = @ptrCast(&keyboardHandleKeymap), + .enter = @ptrCast(&keyboardHandleEnter), + .leave = @ptrCast(&keyboardHandleLeave), + .key = @ptrCast(&keyboardHandleKey), + .modifiers = @ptrCast(&keyboardHandleModifiers), + .repeat_info = @ptrCast(&keyboardHandleRepeatInfo), + }, user_data); + } + + if ((caps & c.WL_SEAT_CAPABILITY_TOUCH) != 0) { + //TODO + } + + if ((caps & c.WL_SEAT_CAPABILITY_POINTER) != 0) { + user_data.pointer = c.wl_seat_get_pointer(seat); + + //TODO: handle return value + _ = c.wl_pointer_add_listener(user_data.pointer, &.{ + .axis = @ptrCast(&handlePointerAxis), + .axis_discrete = @ptrCast(&handlePointerAxisDiscrete), + .axis_relative_direction = @ptrCast(&handlePointerAxisRelativeDirection), + .axis_source = @ptrCast(&handlePointerAxisSource), + .axis_stop = @ptrCast(&handlePointerAxisStop), + .axis_value120 = @ptrCast(&handlePointerAxisValue120), + .button = @ptrCast(&handlePointerButton), + .enter = @ptrCast(&handlePointerEnter), + .frame = @ptrCast(&handlePointerFrame), + .leave = @ptrCast(&handlePointerLeave), + .motion = @ptrCast(&handlePointerMotion), + }, user_data); + } + + // Delete keyboard if its no longer in the seat + if (user_data.keyboard) |keyboard| { + if ((caps & c.WL_SEAT_CAPABILITY_KEYBOARD) == 0) { + c.wl_keyboard_destroy(keyboard); + user_data.keyboard = null; + } + } + + if (user_data.pointer) |pointer| { + if ((caps & c.WL_SEAT_CAPABILITY_POINTER) == 0) { + c.wl_pointer_destroy(pointer); + user_data.pointer = null; + } + } +} + +fn handlePointerEnter(user_data: *GlobalState, pointer: ?*c.struct_wl_pointer, serial: u32, surface: ?*c.struct_wl_surface, fixed_x: c.wl_fixed_t, fixed_y: c.wl_fixed_t) callconv(.C) void { + _ = fixed_x; // autofix + _ = fixed_y; // autofix + _ = user_data; // autofix + _ = pointer; // autofix + _ = serial; // autofix + _ = surface; // autofix +} +fn handlePointerLeave(user_data: *GlobalState, pointer: ?*c.struct_wl_pointer, serial: u32, surface: ?*c.struct_wl_surface) callconv(.C) void { + _ = user_data; // autofix + _ = pointer; // autofix + _ = serial; // autofix + _ = surface; // autofix +} +fn handlePointerMotion(user_data: *GlobalState, pointer: ?*c.struct_wl_pointer, serial: u32, fixed_x: c.wl_fixed_t, fixed_y: c.wl_fixed_t) callconv(.C) void { + _ = pointer; // autofix + _ = serial; // autofix + + const x = c.wl_fixed_to_double(fixed_x); + const y = c.wl_fixed_to_double(fixed_y); + + user_data.pushEvent(.{ .mouse_motion = .{ .pos = .{ .x = x, .y = y } } }); + user_data.input_state.mouse_position = .{ .x = x, .y = y }; +} +fn handlePointerButton(user_data: *GlobalState, pointer: ?*c.struct_wl_pointer, serial: u32, time: u32, button: u32, state: u32) callconv(.C) void { + _ = pointer; // autofix + _ = serial; // autofix + _ = time; // autofix + + const mouse_button: MouseButton = @enumFromInt(button - c.BTN_LEFT); + const pressed = state == c.WL_POINTER_BUTTON_STATE_PRESSED; + + user_data.input_state.mouse_buttons.setValue(@intFromEnum(mouse_button), pressed); + + if (pressed) { + user_data.pushEvent(Event{ .mouse_press = .{ + .button = mouse_button, + .mods = user_data.modifiers, + .pos = user_data.input_state.mouse_position, + } }); + } else { + user_data.pushEvent(Event{ .mouse_release = .{ + .button = mouse_button, + .mods = user_data.modifiers, + .pos = user_data.input_state.mouse_position, + } }); + } +} +fn handlePointerAxis(user_data: *GlobalState, pointer: ?*c.struct_wl_pointer, time: u32, axis: u32, value: c.wl_fixed_t) callconv(.C) void { + _ = user_data; // autofix + _ = pointer; // autofix + _ = time; // autofix + _ = axis; // autofix + _ = value; // autofix +} +fn handlePointerFrame(user_data: *GlobalState, pointer: ?*c.struct_wl_pointer) callconv(.C) void { + _ = user_data; // autofix + _ = pointer; // autofix +} +fn handlePointerAxisSource(user_data: *GlobalState, pointer: ?*c.struct_wl_pointer, axis_source: u32) callconv(.C) void { + _ = user_data; // autofix + _ = pointer; // autofix + _ = axis_source; // autofix +} +fn handlePointerAxisStop(user_data: *GlobalState, pointer: ?*c.struct_wl_pointer, time: u32, axis: u32) callconv(.C) void { + _ = user_data; // autofix + _ = pointer; // autofix + _ = time; // autofix + _ = axis; // autofix +} +fn handlePointerAxisDiscrete(user_data: *GlobalState, pointer: ?*c.struct_wl_pointer, axis: u32, discrete: i32) callconv(.C) void { + _ = user_data; // autofix + _ = pointer; // autofix + _ = axis; // autofix + _ = discrete; // autofix +} +fn handlePointerAxisValue120(user_data: *GlobalState, pointer: ?*c.struct_wl_pointer, axis: u32, value_120: i32) callconv(.C) void { + _ = user_data; // autofix + _ = pointer; // autofix + _ = axis; // autofix + _ = value_120; // autofix +} +fn handlePointerAxisRelativeDirection(user_data: *GlobalState, pointer: ?*c.struct_wl_pointer, axis: u32, direction: u32) callconv(.C) void { + _ = user_data; // autofix + _ = pointer; // autofix + _ = axis; // autofix + _ = direction; // autofix +} + +fn keyboardHandleKeymap(user_data: *GlobalState, keyboard: ?*c.struct_wl_keyboard, format: u32, fd: i32, keymap_size: u32) callconv(.C) void { + _ = keyboard; + + if (format != c.WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) { + @panic("TODO"); + } + + const map_str = std.os.mmap(null, keymap_size, std.os.PROT.READ, std.os.MAP.SHARED, fd, 0) catch unreachable; + + const keymap = user_data.libxkbcommon.xkb_keymap_new_from_string( + user_data.xkb_context, + @alignCast(map_str), //align cast happening here, im sure its fine? TODO: figure out if this okay + c.XKB_KEYMAP_FORMAT_TEXT_V1, + 0, + ).?; + log.debug("got keymap {*}", .{keymap}); + + //Unmap the keymap + std.os.munmap(map_str); + //Close the fd + std.os.close(fd); + + const state = user_data.libxkbcommon.xkb_state_new(keymap).?; + // defer user_data.libxkbcommon.xkb_state_unref(state); + + //this chain hurts me. why must C be this way. + const locale = std.os.getenv("LC_ALL") orelse std.os.getenv("LC_CTYPE") orelse std.os.getenv("LANG") orelse "C"; + + var compose_table = user_data.libxkbcommon.xkb_compose_table_new_from_locale( + user_data.xkb_context, + locale, + c.XKB_COMPOSE_COMPILE_NO_FLAGS, + ); + + //If creation failed, lets try the C locale + if (compose_table == null) + compose_table = user_data.libxkbcommon.xkb_compose_table_new_from_locale( + user_data.xkb_context, + "C", + c.XKB_COMPOSE_COMPILE_NO_FLAGS, + ).?; + + defer user_data.libxkbcommon.xkb_compose_table_unref(compose_table); + + user_data.keymap = keymap; + user_data.xkb_state = state; + user_data.compose_state = user_data.libxkbcommon.xkb_compose_state_new(compose_table, c.XKB_COMPOSE_STATE_NO_FLAGS).?; + + user_data.control_index = user_data.libxkbcommon.xkb_keymap_mod_get_index(keymap, "Control"); + user_data.alt_index = user_data.libxkbcommon.xkb_keymap_mod_get_index(keymap, "Mod1"); + user_data.shift_index = user_data.libxkbcommon.xkb_keymap_mod_get_index(keymap, "Shift"); + user_data.super_index = user_data.libxkbcommon.xkb_keymap_mod_get_index(keymap, "Mod4"); + user_data.caps_lock_index = user_data.libxkbcommon.xkb_keymap_mod_get_index(keymap, "Lock"); + user_data.num_lock_index = user_data.libxkbcommon.xkb_keymap_mod_get_index(keymap, "Mod2"); +} +fn keyboardHandleEnter(user_data: *GlobalState, keyboard: ?*c.struct_wl_keyboard, serial: u32, surface: ?*c.struct_wl_surface, keys: [*c]c.struct_wl_array) callconv(.C) void { + _ = keyboard; + _ = serial; + _ = surface; + _ = keys; + + user_data.pushEvent(.focus_gained); +} +fn keyboardHandleLeave(user_data: *GlobalState, keyboard: ?*c.struct_wl_keyboard, serial: u32, surface: ?*c.struct_wl_surface) callconv(.C) void { + _ = keyboard; + _ = serial; + _ = surface; + + user_data.pushEvent(.focus_lost); +} +fn keyboardHandleKey(user_data: *GlobalState, keyboard: ?*c.struct_wl_keyboard, serial: u32, time: u32, scancode: u32, state: u32) callconv(.C) void { + _ = keyboard; + _ = serial; + _ = time; + + const key = toMachKey(scancode); + const pressed = state == 1; + + user_data.input_state.keys.setValue(@intFromEnum(key), pressed); + + if (pressed) { + user_data.pushEvent(Event{ .key_press = .{ + .key = key, + .mods = user_data.modifiers, + } }); + + var keysyms: ?[*]c.xkb_keysym_t = undefined; + //Get the keysym from the keycode (scancode + 8) + if (user_data.libxkbcommon.xkb_state_key_get_syms(user_data.xkb_state, scancode + 8, &keysyms) == 1) { + //Compose the keysym + const keysym: c.xkb_keysym_t = composeSymbol(user_data, keysyms.?[0]); + + //Try to convert that keysym to a unicode codepoint + if (Unicode.unicodeFromKeySym(keysym)) |codepoint| { + user_data.pushEvent(Event{ .char_input = .{ .codepoint = codepoint } }); + } + } + } else { + user_data.pushEvent(Event{ .key_release = .{ + .key = key, + .mods = user_data.modifiers, + } }); + } +} +fn keyboardHandleModifiers(user_data: *GlobalState, keyboard: ?*c.struct_wl_keyboard, serial: u32, mods_depressed: u32, mods_latched: u32, mods_locked: u32, group: u32) callconv(.C) void { + _ = keyboard; + _ = serial; + + if (user_data.keymap == null) + return; + + //TODO: handle this return value + _ = user_data.libxkbcommon.xkb_state_update_mask( + user_data.xkb_state.?, + mods_depressed, + mods_latched, + mods_locked, + 0, + 0, + group, + ); + + //Iterate over all the modifiers + inline for (.{ + .{ user_data.alt_index, "alt" }, + .{ user_data.shift_index, "shift" }, + .{ user_data.super_index, "super" }, + .{ user_data.control_index, "control" }, + .{ user_data.num_lock_index, "num_lock" }, + .{ user_data.caps_lock_index, "caps_lock" }, + }) |key| { + @field(user_data.modifiers, key[1]) = user_data.libxkbcommon.xkb_state_mod_index_is_active( + user_data.xkb_state, + key[0], + c.XKB_STATE_MODS_EFFECTIVE, + ) == 1; + } +} +fn keyboardHandleRepeatInfo(user_data: *GlobalState, keyboard: ?*c.struct_wl_keyboard, rate: i32, delay: i32) callconv(.C) void { + _ = user_data; + _ = keyboard; + _ = rate; + _ = delay; +} + +fn composeSymbol(user_data: *GlobalState, sym: c.xkb_keysym_t) c.xkb_keysym_t { + if (sym == c.XKB_KEY_NoSymbol or user_data.compose_state == null) + return sym; + + if (user_data.libxkbcommon.xkb_compose_state_feed(user_data.compose_state, sym) != c.XKB_COMPOSE_FEED_ACCEPTED) + return sym; + + return switch (user_data.libxkbcommon.xkb_compose_state_get_status(user_data.compose_state)) { + c.XKB_COMPOSE_COMPOSED => user_data.libxkbcommon.xkb_compose_state_get_one_sym(user_data.compose_state), + c.XKB_COMPOSE_COMPOSING, c.XKB_COMPOSE_CANCELLED => c.XKB_KEY_NoSymbol, + else => sym, + }; +} + +fn wmBaseHandlePing(user_data: *GlobalState, wm_base: ?*c.struct_xdg_wm_base, serial: u32) callconv(.C) void { + _ = user_data; + + log.debug("Got wm base ping {*} with serial {d}", .{ wm_base, serial }); + + c.xdg_wm_base_pong(wm_base, serial); +} + +fn registryHandleGlobalRemove(user_data: *GlobalState, registry: ?*c.struct_wl_registry, name: u32) callconv(.C) void { + _ = user_data; + _ = registry; + _ = name; +} + +const registry_listener = c.wl_registry_listener{ + // ptrcast is for the [*:0] -> [*c] conversion, silly yes + .global = @ptrCast(®istryHandleGlobal), + // ptrcast is for the user_data param, which is guarenteed to be our type (and if its not, it should be caught by safety checks) + .global_remove = @ptrCast(®istryHandleGlobalRemove), +}; + +fn xdgSurfaceHandleConfigure(user_data: *GlobalState, xdg_surface: ?*c.struct_xdg_surface, serial: u32) callconv(.C) void { + c.xdg_surface_ack_configure(xdg_surface, serial); + + if (user_data.configured) { + c.wl_surface_commit(user_data.surface); + } else { + log.debug("xdg surface configured", .{}); + user_data.configured = true; + } + + user_data.state_mu.lock(); + defer user_data.state_mu.unlock(); + + if (user_data.window_size.read()) |new_window_size| { + setContentAreaOpaque(user_data, new_window_size); + } +} + +fn xdgToplevelHandleClose(user_data: *GlobalState, toplevel: ?*c.struct_xdg_toplevel) callconv(.C) void { + _ = user_data; + _ = toplevel; +} + +fn xdgToplevelHandleConfigure(user_data: *GlobalState, toplevel: ?*c.struct_xdg_toplevel, width: i32, height: i32, states: [*c]c.struct_wl_array) callconv(.C) void { + _ = toplevel; + _ = states; + + log.debug("{d}/{d}", .{ width, height }); + + if (width > 0 and height > 0) { + user_data.state_mu.lock(); + defer user_data.state_mu.unlock(); + + try user_data.window_size.set(.{ .width = @intCast(width), .height = @intCast(height) }); + } +} + +fn setContentAreaOpaque(state: *GlobalState, new_size: Size) void { + const region = c.wl_compositor_create_region(state.interfaces.wl_compositor) orelse return; + + c.wl_region_add(region, 0, 0, @intCast(new_size.width), @intCast(new_size.height)); + c.wl_surface_set_opaque_region(state.surface, region); + c.wl_region_destroy(region); + + state.swap_chain_update.set(); +} + +// Called on the main thread +pub fn init( + core: *Core, + allocator: std.mem.Allocator, + frame: *Frequency, + input: *Frequency, + options: Options, +) !void { + core.global_state = .{ + .interfaces = .{}, + .configured = false, + .surface = null, + .events = EventQueue.init(allocator), + .libxkbcommon = try LibXkbCommon.load(), + .xkb_context = null, + .keymap = null, + .xkb_state = null, + .compose_state = null, + .alt_index = undefined, + .shift_index = undefined, + .super_index = undefined, + .control_index = undefined, + .caps_lock_index = undefined, + .num_lock_index = undefined, + .input_state = .{}, + .modifiers = .{ + .alt = false, + .caps_lock = false, + .control = false, + .num_lock = false, + .shift = false, + .super = false, + }, + .window_size = try @TypeOf(core.global_state.window_size).init(options.size, {}), + }; + + libwaylandclient = try LibWaylandClient.load(); + + core.global_state.xkb_context = core.global_state.libxkbcommon.xkb_context_new(0).?; + + core.display = libwaylandclient.wl_display_connect(null) orelse return error.FailedToConnectToWaylandDisplay; + + const registry = c.wl_display_get_registry(core.display) orelse return error.FailedToGetDisplayRegistry; + // TODO: handle error return value here + _ = c.wl_registry_add_listener(registry, ®istry_listener, &core.global_state); + + //Round trip to get all the registry objects + _ = libwaylandclient.wl_display_roundtrip(core.display); + + //Round trip to get all initial output events + _ = libwaylandclient.wl_display_roundtrip(core.display); + + core.global_state.surface = c.wl_compositor_create_surface(core.global_state.interfaces.wl_compositor) orelse return error.UnableToCreateSurface; + log.debug("Got surface {*}", .{core.global_state.surface}); + + var tag: [*:0]c_char = undefined; + libwaylandclient.wl_proxy_set_tag(@ptrCast(core.global_state.surface), @ptrCast(&tag)); + + { + const region = c.wl_compositor_create_region(core.global_state.interfaces.wl_compositor) orelse return error.CouldntCreateWaylandRegtion; + + c.wl_region_add( + region, + 0, + 0, + @intCast(options.size.width), + @intCast(options.size.height), + ); + c.wl_surface_set_opaque_region(core.global_state.surface, region); + c.wl_region_destroy(region); + } + + const xdg_surface = c.xdg_wm_base_get_xdg_surface(core.global_state.interfaces.xdg_wm_base, core.global_state.surface) orelse return error.UnableToCreateXdgSurface; + log.debug("Got xdg surface {*}", .{xdg_surface}); + + const toplevel = c.xdg_surface_get_toplevel(xdg_surface) orelse return error.UnableToGetXdgTopLevel; + log.debug("Got xdg toplevel {*}", .{toplevel}); + + core.min_size = try @TypeOf(core.min_size).init(.{ .width = 0, .height = 0 }, {}); + core.max_size = try @TypeOf(core.max_size).init(.{ .width = 0, .height = 0 }, {}); + + //TODO: handle this return value + _ = c.xdg_surface_add_listener(xdg_surface, &.{ .configure = @ptrCast(&xdgSurfaceHandleConfigure) }, &core.global_state); + + //TODO: handle this return value + _ = c.xdg_toplevel_add_listener(toplevel, &.{ + .configure = @ptrCast(&xdgToplevelHandleConfigure), + .close = @ptrCast(&xdgToplevelHandleClose), + }, &core.global_state); + + //Commit changes to surface + c.wl_surface_commit(core.global_state.surface); + + while (libwaylandclient.wl_display_dispatch(core.display) != -1 and !core.global_state.configured) { + // This space intentionally left blank + } + + core.title = try @TypeOf(core.title).init(options.title, allocator); + + c.xdg_toplevel_set_title(toplevel, core.title.current); + + const decoration = c.zxdg_decoration_manager_v1_get_toplevel_decoration( + core.global_state.interfaces.zxdg_decoration_manager_v1, + toplevel, + ) orelse return error.UnableToGetToplevelDecoration; + log.debug("Got xdg toplevel decoration {*}", .{decoration}); + + c.zxdg_toplevel_decoration_v1_set_mode( + decoration, + c.ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE, + ); + + //Commit changes to surface + c.wl_surface_commit(core.global_state.surface); + //TODO: handle return value + _ = libwaylandclient.wl_display_roundtrip(core.display); + + const instance = gpu.createInstance(null) orelse { + log.err("failed to create GPU instance", .{}); + std.process.exit(1); + }; + const surface = instance.createSurface(&gpu.Surface.Descriptor{ + .next_in_chain = .{ + .from_wayland_surface = &.{ + .display = core.display, + .surface = core.global_state.surface.?, + }, + }, + }); + + var response: RequestAdapterResponse = undefined; + instance.requestAdapter(&gpu.RequestAdapterOptions{ + .compatible_surface = surface, + .power_preference = options.power_preference, + .force_fallback_adapter = .false, + }, &response, requestAdapterCallback); + if (response.status != .success) { + log.err("failed to create GPU adapter: {?s}", .{response.message}); + log.info("-> maybe try MACH_GPU_BACKEND=opengl ?", .{}); + std.process.exit(1); + } + + // Print which adapter we are going to use. + var props = std.mem.zeroes(gpu.Adapter.Properties); + response.adapter.?.getProperties(&props); + if (props.backend_type == .null) { + log.err("no backend found for {s} adapter", .{props.adapter_type.name()}); + std.process.exit(1); + } + log.info("found {s} backend on {s} adapter: {s}, {s}\n", .{ + props.backend_type.name(), + props.adapter_type.name(), + props.name, + props.driver_description, + }); + + // Create a device with default limits/features. + const gpu_device = response.adapter.?.createDevice(&.{ + .next_in_chain = .{ + .dawn_toggles_descriptor = &gpu.dawn.TogglesDescriptor.init(.{ + .enabled_toggles = &[_][*:0]const u8{ + "allow_unsafe_apis", + }, + }), + }, + + .required_features_count = if (options.required_features) |v| @as(u32, @intCast(v.len)) else 0, + .required_features = if (options.required_features) |v| @as(?[*]const gpu.FeatureName, v.ptr) else null, + .required_limits = if (options.required_limits) |limits| @as(?*const gpu.RequiredLimits, &gpu.RequiredLimits{ + .limits = limits, + }) else null, + .device_lost_callback = &deviceLostCallback, + .device_lost_userdata = null, + }) orelse { + log.err("failed to create GPU device\n", .{}); + std.process.exit(1); + }; + gpu_device.setUncapturedErrorCallback({}, printUnhandledErrorCallback); + + const swap_chain_desc = gpu.SwapChain.Descriptor{ + .label = "main swap chain", + .usage = .{ .render_attachment = true }, + .format = .bgra8_unorm, + .width = options.size.width, + .height = options.size.height, + .present_mode = .mailbox, + }; + const swap_chain = gpu_device.createSwapChain(surface, &swap_chain_desc); + + mach_core.adapter = response.adapter.?; + mach_core.device = gpu_device; + mach_core.queue = gpu_device.getQueue(); + mach_core.swap_chain = swap_chain; + mach_core.descriptor = swap_chain_desc; + + log.debug("DONE", .{}); + + core.* = .{ + .display = core.display, + .registry = registry, + .tag = tag, + .xdg_surface = xdg_surface, + .toplevel = toplevel, + .decoration = decoration, + .gpu_device = gpu_device, + .title = core.title, + .min_size = core.min_size, + .max_size = core.max_size, + .frame = frame, + .input = input, + .global_state = core.global_state, + .swap_chain = swap_chain, + .swap_chain_desc = swap_chain_desc, + .surface = surface, + }; +} + +pub fn deinit(self: *Core) void { + self.title.deinit(); +} + +// Called on the main thread +pub fn update(self: *Core, app: anytype) !bool { + if (self.done.isSet()) return true; + + if (!self.app_update_thread_started) { + self.app_update_thread_started = true; + const thread = try std.Thread.spawn(.{}, appUpdateThread, .{ self, app }); + thread.detach(); + } + + //State updates + { + self.global_state.state_mu.lock(); + defer self.global_state.state_mu.unlock(); + + var need_surface_commit: bool = false; + + // Check if we have a new title + if (self.title.read()) |new_title| { + defer self.title.freeLast(); + + c.xdg_toplevel_set_title(self.toplevel, new_title); + } + + // Check if we have a new min size + if (self.min_size.read()) |new_min_size| { + c.xdg_toplevel_set_min_size(self.toplevel, @intCast(new_min_size.width), @intCast(new_min_size.height)); + + need_surface_commit = true; + } + + // Check if we have a new max size + if (self.max_size.read()) |new_max_size| { + c.xdg_toplevel_set_max_size(self.toplevel, @intCast(new_max_size.width), @intCast(new_max_size.height)); + + need_surface_commit = true; + } + + if (need_surface_commit) + c.wl_surface_commit(self.global_state.surface); + } + + // while (libwaylandclient.wl_display_flush(self.display) == -1) { + // // if (std.os.errno() == std.os.E.AGAIN) { + // // log.err("flush error", .{}); + // // return true; + // // } + + // var pollfd = [_]std.os.pollfd{ + // std.os.pollfd{ + // .fd = libwaylandclient.wl_display_get_fd(self.display), + // .events = std.os.POLL.OUT, + // .revents = 0, + // }, + // }; + + // while (try std.os.poll(&pollfd, -1) != 0) { + // // if (std.os.errno() == std.os.E.INTR or std.os.errno() == std.os.E.AGAIN) { + // // log.err("poll error", .{}); + // // return true; + // // } + // } + // } + + if (@hasDecl(std.meta.Child(@TypeOf(app)), "updateMainThread")) { + if (app.updateMainThread() catch unreachable) { + self.done.set(); + return true; + } + } + + _ = libwaylandclient.wl_display_roundtrip(self.display); + + self.input.tick(); + return false; +} + +// Secondary app-update thread +pub fn appUpdateThread(self: *Core, app: anytype) void { + // @panic("TODO: implement appUpdateThread for Wayland"); + + self.frame.start() catch unreachable; + while (true) { + if (self.global_state.swap_chain_update.isSet()) { // blk: { + self.global_state.swap_chain_update.reset(); + + //TODO + // if (self.current_vsync_mode != self.last_vsync_mode) { + // self.last_vsync_mode = self.current_vsync_mode; + // switch (self.current_vsync_mode) { + // .triple => self.frame.target = 2 * self.refresh_rate, + // else => self.frame.target = 0, + // } + // } + + //TODO + // if (self.current_size.width == 0 or self.current_size.height == 0) break :blk; + + self.global_state.swap_chain_mu.lock(); + defer self.global_state.swap_chain_mu.unlock(); + mach_core.swap_chain.release(); + self.swap_chain_desc.width = self.global_state.window_size.current.width; + self.swap_chain_desc.height = self.global_state.window_size.current.height; + self.swap_chain = self.gpu_device.createSwapChain(self.surface, &self.swap_chain_desc); + + mach_core.swap_chain = self.swap_chain; + mach_core.descriptor = self.swap_chain_desc; + + self.global_state.pushEvent(.{ + .framebuffer_resize = .{ + .width = self.global_state.window_size.current.width, + .height = self.global_state.window_size.current.height, + }, + }); + } + + if (app.update() catch unreachable) { + self.done.set(); + + // Wake the main thread from any event handling, so there is not e.g. a one second delay + // in exiting the application. + // self.wakeMainThread(); + return; + } + self.gpu_device.tick(); + self.gpu_device.machWaitForCommandsToBeScheduled(); + + self.frame.tick(); + if (self.frame.delay_ns != 0) std.time.sleep(self.frame.delay_ns); + } +} + +// May be called from any thread. +pub inline fn pollEvents(self: *Core) EventIterator { + return EventIterator{ .events_mu = &self.global_state.events_mu, .queue = &self.global_state.events }; +} + +// May be called from any thread. +pub fn setTitle(self: *Core, title: [:0]const u8) void { + self.global_state.state_mu.lock(); + defer self.global_state.state_mu.unlock(); + + self.title.set(title) catch unreachable; +} + +// May be called from any thread. +pub fn setDisplayMode(_: *Core, _: DisplayMode) void { + @panic("TODO: implement setDisplayMode for Wayland"); +} + +// May be called from any thread. +pub fn displayMode(_: *Core) DisplayMode { + @panic("TODO: implement displayMode for Wayland"); +} + +// May be called from any thread. +pub fn setBorder(_: *Core, _: bool) void { + @panic("TODO: implement setBorder for Wayland"); +} + +// May be called from any thread. +pub fn border(_: *Core) bool { + @panic("TODO: implement border for Wayland"); +} + +// May be called from any thread. +pub fn setHeadless(_: *Core, _: bool) void { + @panic("TODO: implement setHeadless for Wayland"); +} + +// May be called from any thread. +pub fn headless(_: *Core) bool { + @panic("TODO: implement headless for Wayland"); +} + +// May be called from any thread. +pub fn setVSync(_: *Core, _: VSyncMode) void { + @panic("TODO: implement setVSync for Wayland"); +} + +// May be called from any thread. +pub fn vsync(_: *Core) VSyncMode { + @panic("TODO: implement vsync for Wayland"); +} + +// May be called from any thread. +pub fn setSize(self: *Core, new_size: Size) void { + self.global_state.window_size_mu.lock(); + defer self.global_state.window_size_mu.unlock(); + + setContentAreaOpaque(&self.global_state, new_size); + self.global_state.window_size.set(new_size) catch unreachable; +} + +// May be called from any thread. +pub fn size(self: *Core) Size { + self.state_mu.lock(); + defer self.state_mu.unlock(); + + return self.window_size.current; +} + +// May be called from any thread. +pub fn setSizeLimit(self: *Core, limits: SizeLimit) void { + self.state_mu.lock(); + defer self.state_mu.unlock(); + + if (limits.max.width) |width| if (width == 0) @panic("todo: what do we do here?"); + if (limits.max.height) |height| if (height == 0) @panic("todo: what do we do here?"); + if (limits.min.width) |width| if (width == 0) @panic("todo: what do we do here?"); + if (limits.min.height) |height| if (height == 0) @panic("todo: what do we do here?"); + + //TODO: only set the changed one, not both! + self.min_size.set(.{ + .width = limits.min.width orelse 0, + .height = limits.min.height orelse 0, + }); + self.max_size.set(.{ + .width = limits.max.width orelse 0, + .height = limits.max.height orelse 0, + }); +} + +// May be called from any thread. +pub fn sizeLimit(self: *Core) SizeLimit { + self.state_mu.lock(); + defer self.state_mu.unlock(); + + return SizeLimit{ + .max = .{ + .width = if (self.max_size.current.width == 0) null else self.max_size.current.width, + .height = if (self.max_size.current.height == 0) null else self.max_size.current.height, + }, + .min = .{ + .width = if (self.min_size.current.width == 0) null else self.min_size.current.width, + .height = if (self.min_size.current.height == 0) null else self.min_size.current.height, + }, + }; +} + +// May be called from any thread. +pub fn setCursorMode(_: *Core, _: CursorMode) void { + @panic("TODO: implement setCursorMode for Wayland"); +} + +// May be called from any thread. +pub fn cursorMode(_: *Core) CursorMode { + @panic("TODO: implement cursorMode for Wayland"); +} + +// May be called from any thread. +pub fn setCursorShape(_: *Core, _: CursorShape) void { + @panic("TODO: implement setCursorShape for Wayland"); +} + +// May be called from any thread. +pub fn cursorShape(_: *Core) CursorShape { + @panic("TODO: implement cursorShape for Wayland"); +} + +// May be called from any thread. +pub fn joystickPresent(_: *Core, _: Joystick) bool { + @panic("TODO: implement joystickPresent for Wayland"); +} + +// May be called from any thread. +pub fn joystickName(_: *Core, _: Joystick) ?[:0]const u8 { + @panic("TODO: implement joystickName for Wayland"); +} + +// May be called from any thread. +pub fn joystickButtons(_: *Core, _: Joystick) ?[]const bool { + @panic("TODO: implement joystickButtons for Wayland"); +} + +// May be called from any thread. +pub fn joystickAxes(_: *Core, _: Joystick) ?[]const f32 { + @panic("TODO: implement joystickAxes for Wayland"); +} + +// May be called from any thread. +pub fn keyPressed(self: *Core, key: Key) bool { + self.input_state.isKeyPressed(key); +} + +// May be called from any thread. +pub fn keyReleased(self: *Core, key: Key) bool { + self.input_state.isKeyReleased(key); +} + +// May be called from any thread. +pub fn mousePressed(self: *Core, button: MouseButton) bool { + return self.input_state.isMouseButtonPressed(button); +} + +// May be called from any thread. +pub fn mouseReleased(self: *Core, button: MouseButton) bool { + return self.input_state.isMouseButtonReleased(button); +} + +// May be called from any thread. +pub fn mousePosition(self: *Core) mach_core.Position { + return self.mouse_pos; +} + +// May be called from any thread. +pub inline fn outOfMemory(_: *Core) bool { + @panic("TODO: implement outOfMemory for Wayland"); +} + +// TODO(important): expose device loss to users, this can happen especially in the web and on mobile +// devices. Users will need to re-upload all assets to the GPU in this event. +fn deviceLostCallback(reason: gpu.Device.LostReason, msg: [*:0]const u8, userdata: ?*anyopaque) callconv(.C) void { + _ = userdata; + _ = reason; + log.err("mach: device lost: {s}", .{msg}); + @panic("mach: device lost"); +} + +fn toMachKey(key: u32) Key { + return switch (key) { + c.KEY_GRAVE => .grave, + c.KEY_1 => .one, + c.KEY_2 => .two, + c.KEY_3 => .three, + c.KEY_4 => .four, + c.KEY_5 => .five, + c.KEY_6 => .six, + c.KEY_7 => .seven, + c.KEY_8 => .eight, + c.KEY_9 => .nine, + c.KEY_0 => .zero, + c.KEY_SPACE => .space, + c.KEY_MINUS => .minus, + c.KEY_EQUAL => .equal, + c.KEY_Q => .q, + c.KEY_W => .w, + c.KEY_E => .e, + c.KEY_R => .r, + c.KEY_T => .t, + c.KEY_Y => .y, + c.KEY_U => .u, + c.KEY_I => .i, + c.KEY_O => .o, + c.KEY_P => .p, + c.KEY_LEFTBRACE => .left_bracket, + c.KEY_RIGHTBRACE => .right_bracket, + c.KEY_A => .a, + c.KEY_S => .s, + c.KEY_D => .d, + c.KEY_F => .f, + c.KEY_G => .g, + c.KEY_H => .h, + c.KEY_J => .j, + c.KEY_K => .k, + c.KEY_L => .l, + c.KEY_SEMICOLON => .semicolon, + c.KEY_APOSTROPHE => .apostrophe, + c.KEY_Z => .z, + c.KEY_X => .x, + c.KEY_C => .c, + c.KEY_V => .v, + c.KEY_B => .b, + c.KEY_N => .n, + c.KEY_M => .m, + c.KEY_COMMA => .comma, + c.KEY_DOT => .period, + c.KEY_SLASH => .slash, + c.KEY_BACKSLASH => .backslash, + c.KEY_ESC => .escape, + c.KEY_TAB => .tab, + c.KEY_LEFTSHIFT => .left_shift, + c.KEY_RIGHTSHIFT => .right_shift, + c.KEY_LEFTCTRL => .left_control, + c.KEY_RIGHTCTRL => .right_control, + c.KEY_LEFTALT => .left_alt, + c.KEY_RIGHTALT => .right_alt, + c.KEY_LEFTMETA => .left_super, + c.KEY_RIGHTMETA => .right_super, + c.KEY_NUMLOCK => .num_lock, + c.KEY_CAPSLOCK => .caps_lock, + c.KEY_PRINT => .print, + c.KEY_SCROLLLOCK => .scroll_lock, + c.KEY_PAUSE => .pause, + c.KEY_DELETE => .delete, + c.KEY_BACKSPACE => .backspace, + c.KEY_ENTER => .enter, + c.KEY_HOME => .home, + c.KEY_END => .end, + c.KEY_PAGEUP => .page_up, + c.KEY_PAGEDOWN => .page_down, + c.KEY_INSERT => .insert, + c.KEY_LEFT => .left, + c.KEY_RIGHT => .right, + c.KEY_DOWN => .down, + c.KEY_UP => .up, + c.KEY_F1 => .f1, + c.KEY_F2 => .f2, + c.KEY_F3 => .f3, + c.KEY_F4 => .f4, + c.KEY_F5 => .f5, + c.KEY_F6 => .f6, + c.KEY_F7 => .f7, + c.KEY_F8 => .f8, + c.KEY_F9 => .f9, + c.KEY_F10 => .f10, + c.KEY_F11 => .f11, + c.KEY_F12 => .f12, + c.KEY_F13 => .f13, + c.KEY_F14 => .f14, + c.KEY_F15 => .f15, + c.KEY_F16 => .f16, + c.KEY_F17 => .f17, + c.KEY_F18 => .f18, + c.KEY_F19 => .f19, + c.KEY_F20 => .f20, + c.KEY_F21 => .f21, + c.KEY_F22 => .f22, + c.KEY_F23 => .f23, + c.KEY_F24 => .f24, + c.KEY_KPSLASH => .kp_divide, + c.KEY_KPASTERISK => .kp_multiply, + c.KEY_KPMINUS => .kp_subtract, + c.KEY_KPPLUS => .kp_add, + c.KEY_KP0 => .kp_0, + c.KEY_KP1 => .kp_1, + c.KEY_KP2 => .kp_2, + c.KEY_KP3 => .kp_3, + c.KEY_KP4 => .kp_4, + c.KEY_KP5 => .kp_5, + c.KEY_KP6 => .kp_6, + c.KEY_KP7 => .kp_7, + c.KEY_KP8 => .kp_8, + c.KEY_KP9 => .kp_9, + c.KEY_KPDOT => .kp_decimal, + c.KEY_KPEQUAL => .kp_equal, + c.KEY_KPENTER => .kp_enter, + else => .unknown, + }; +} diff --git a/src/core/platform/wayland/wayland.c b/src/core/platform/wayland/wayland.c new file mode 100644 index 00000000..9d76e005 --- /dev/null +++ b/src/core/platform/wayland/wayland.c @@ -0,0 +1,7 @@ +#include "wayland-client-protocol-code.h" +#include "wayland-xdg-shell-client-protocol-code.h" +#include "wayland-xdg-decoration-client-protocol-code.h" +#include "wayland-viewporter-client-protocol-code.h" +#include "wayland-relative-pointer-unstable-v1-client-protocol-code.h" +#include "wayland-pointer-constraints-unstable-v1-client-protocol-code.h" +#include "wayland-idle-inhibit-unstable-v1-client-protocol-code.h" \ No newline at end of file diff --git a/src/core/platform/x11.zig b/src/core/platform/x11.zig new file mode 100644 index 00000000..bcb444ef --- /dev/null +++ b/src/core/platform/x11.zig @@ -0,0 +1,4 @@ +const std = @import("std"); + +pub const Core = @import("x11/Core.zig"); +pub const Timer = std.time.Timer; diff --git a/src/core/platform/x11/Core.zig b/src/core/platform/x11/Core.zig new file mode 100644 index 00000000..4dfcbe33 --- /dev/null +++ b/src/core/platform/x11/Core.zig @@ -0,0 +1,1609 @@ +const builtin = @import("builtin"); +const std = @import("std"); +const glfw = @import("mach-glfw"); +const mach_core = @import("../../main.zig"); +const gpu = mach_core.gpu; +const unicode = @import("unicode.zig"); +const Options = @import("../../main.zig").Options; +const Event = @import("../../main.zig").Event; +const KeyEvent = @import("../../main.zig").KeyEvent; +const MouseButtonEvent = @import("../../main.zig").MouseButtonEvent; +const MouseButton = @import("../../main.zig").MouseButton; +const Size = @import("../../main.zig").Size; +const DisplayMode = @import("../../main.zig").DisplayMode; +const SizeLimit = @import("../../main.zig").SizeLimit; +const CursorShape = @import("../../main.zig").CursorShape; +const VSyncMode = @import("../../main.zig").VSyncMode; +const CursorMode = @import("../../main.zig").CursorMode; +const Key = @import("../../main.zig").Key; +const KeyMods = @import("../../main.zig").KeyMods; +const Joystick = @import("../../main.zig").Joystick; +const InputState = @import("../../InputState.zig"); +const Frequency = @import("../../Frequency.zig"); +const RequestAdapterResponse = @import("../common.zig").RequestAdapterResponse; +const printUnhandledErrorCallback = @import("../common.zig").printUnhandledErrorCallback; +const detectBackendType = @import("../common.zig").detectBackendType; +const wantGamemode = @import("../common.zig").wantGamemode; +const initLinuxGamemode = @import("../common.zig").initLinuxGamemode; +const deinitLinuxGamemode = @import("../common.zig").deinitLinuxGamemode; +const requestAdapterCallback = @import("../common.zig").requestAdapterCallback; + +pub const c = @cImport({ + @cInclude("X11/Xlib.h"); + @cInclude("X11/Xatom.h"); + @cInclude("X11/cursorfont.h"); + @cInclude("X11/Xcursor/Xcursor.h"); + @cInclude("X11/extensions/Xrandr.h"); +}); + +const log = std.log.scoped(.mach); + +pub const defaultLog = std.log.defaultLog; +pub const defaultPanic = std.debug.panicImpl; + +const LibX11 = struct { + handle: std.DynLib, + + XInitThreads: *const @TypeOf(c.XInitThreads), + XrmInitialize: *const @TypeOf(c.XrmInitialize), + XOpenDisplay: *const @TypeOf(c.XOpenDisplay), + XCloseDisplay: *const @TypeOf(c.XCloseDisplay), + XCreateWindow: *const @TypeOf(c.XCreateWindow), + XSelectInput: *const @TypeOf(c.XSelectInput), + XMapWindow: *const @TypeOf(c.XMapWindow), + XNextEvent: *const @TypeOf(c.XNextEvent), + XDisplayWidth: *const @TypeOf(c.XDisplayWidth), + XDisplayHeight: *const @TypeOf(c.XDisplayHeight), + XCreateColormap: *const @TypeOf(c.XCreateColormap), + XSetErrorHandler: *const @TypeOf(c.XSetErrorHandler), + XGetWindowAttributes: *const @TypeOf(c.XGetWindowAttributes), + XStoreName: *const @TypeOf(c.XStoreName), + XFreeColormap: *const @TypeOf(c.XFreeColormap), + XUnmapWindow: *const @TypeOf(c.XUnmapWindow), + XDestroyWindow: *const @TypeOf(c.XDestroyWindow), + XFlush: *const @TypeOf(c.XFlush), + XLookupString: *const @TypeOf(c.XLookupString), + XQueryPointer: *const @TypeOf(c.XQueryPointer), + XInternAtom: *const @TypeOf(c.XInternAtom), + XSendEvent: *const @TypeOf(c.XSendEvent), + XSetWMProtocols: *const @TypeOf(c.XSetWMProtocols), + XDefineCursor: *const @TypeOf(c.XDefineCursor), + XUndefineCursor: *const @TypeOf(c.XUndefineCursor), + XCreatePixmap: *const @TypeOf(c.XCreatePixmap), + XCreateGC: *const @TypeOf(c.XCreateGC), + XDrawPoint: *const @TypeOf(c.XDrawPoint), + XFreeGC: *const @TypeOf(c.XFreeGC), + XCreatePixmapCursor: *const @TypeOf(c.XCreatePixmapCursor), + XGrabPointer: *const @TypeOf(c.XGrabPointer), + XUngrabPointer: *const @TypeOf(c.XUngrabPointer), + XCreateFontCursor: *const @TypeOf(c.XCreateFontCursor), + XFreeCursor: *const @TypeOf(c.XFreeCursor), + XChangeProperty: *const @TypeOf(c.XChangeProperty), + XResizeWindow: *const @TypeOf(c.XResizeWindow), + XConfigureWindow: *const @TypeOf(c.XConfigureWindow), + XSetWMHints: *const @TypeOf(c.XSetWMHints), + XDeleteProperty: *const @TypeOf(c.XDeleteProperty), + XAllocSizeHints: *const @TypeOf(c.XAllocSizeHints), + XSetWMNormalHints: *const @TypeOf(c.XSetWMNormalHints), + XFree: *const @TypeOf(c.XFree), + + pub fn load() !LibX11 { + var lib: LibX11 = undefined; + lib.handle = std.DynLib.openZ("libX11.so.6") catch return error.LibraryNotFound; + inline for (@typeInfo(LibX11).Struct.fields[1..]) |field| { + const name = std.fmt.comptimePrint("{s}\x00", .{field.name}); + const name_z: [:0]const u8 = @ptrCast(name[0 .. name.len - 1]); + @field(lib, field.name) = lib.handle.lookup(field.type, name_z) orelse return error.SymbolLookup; + } + return lib; + } +}; + +const LibXCursor = struct { + handle: std.DynLib, + + XcursorImageCreate: *const @TypeOf(c.XcursorImageCreate), + XcursorImageDestroy: *const @TypeOf(c.XcursorImageDestroy), + XcursorImageLoadCursor: *const @TypeOf(c.XcursorImageLoadCursor), + XcursorGetTheme: *const @TypeOf(c.XcursorGetTheme), + XcursorGetDefaultSize: *const @TypeOf(c.XcursorGetDefaultSize), + XcursorLibraryLoadImage: *const @TypeOf(c.XcursorLibraryLoadImage), + + pub fn load() !LibXCursor { + var lib: LibXCursor = undefined; + lib.handle = std.DynLib.openZ("libXcursor.so.1") catch return error.LibraryNotFound; + inline for (@typeInfo(LibXCursor).Struct.fields[1..]) |field| { + const name = std.fmt.comptimePrint("{s}\x00", .{field.name}); + const name_z: [:0]const u8 = @ptrCast(name[0 .. name.len - 1]); + @field(lib, field.name) = lib.handle.lookup(field.type, name_z) orelse return error.SymbolLookup; + } + return lib; + } +}; + +const LibXRR = struct { + handle: std.DynLib, + + XRRGetScreenInfo: *const @TypeOf(c.XRRGetScreenInfo), + XRRConfigCurrentRate: *const @TypeOf(c.XRRConfigCurrentRate), + + pub fn load() !LibXRR { + var lib: LibXRR = undefined; + lib.handle = std.DynLib.openZ("libXrandr.so.1") catch return error.LibraryNotFound; + inline for (@typeInfo(LibXRR).Struct.fields[1..]) |field| { + const name = std.fmt.comptimePrint("{s}\x00", .{field.name}); + const name_z: [:0]const u8 = @ptrCast(name[0 .. name.len - 1]); + @field(lib, field.name) = lib.handle.lookup(field.type, name_z) orelse return error.SymbolLookup; + } + return lib; + } +}; + +const LibGL = struct { + const Drawable = c.XID; + const Context = opaque {}; + const FBConfig = opaque {}; + + const rgba = 4; + const doublebuffer = 5; + const red_size = 8; + const green_size = 9; + const blue_size = 10; + const depth_size = 12; + const stencil_size = 13; + const sample_buffers = 0x186a0; + const samples = 0x186a1; + + handle: std.DynLib, + + glXCreateContext: *const fn (*c.Display, *c.XVisualInfo, ?*Context, bool) callconv(.C) ?*Context, + glXDestroyContext: *const fn (*c.Display, ?*Context) callconv(.C) void, + glXMakeCurrent: *const fn (*c.Display, Drawable, ?*Context) callconv(.C) bool, + glXChooseVisual: *const fn (*c.Display, c_int, [*]const c_int) callconv(.C) *c.XVisualInfo, + glXSwapBuffers: *const fn (*c.Display, Drawable) callconv(.C) bool, + + pub fn load() !LibGL { + var lib: LibGL = undefined; + lib.handle = std.DynLib.openZ("libGL.so.1") catch return error.LibraryNotFound; + inline for (@typeInfo(LibGL).Struct.fields[1..]) |field| { + const name = std.fmt.comptimePrint("{s}\x00", .{field.name}); + const name_z: [:0]const u8 = @ptrCast(name[0 .. name.len - 1]); + @field(lib, field.name) = lib.handle.lookup(field.type, name_z) orelse return error.SymbolLookup; + } + return lib; + } +}; + +pub const Core = @This(); + +// There are two threads: +// +// 1. Main thread (App.init, App.deinit) which may interact with GLFW and handles events +// 2. App.update thread. + +// Read-only fields +allocator: std.mem.Allocator, +display: *c.Display, +libx11: LibX11, +libxrr: ?LibXRR, +libgl: ?LibGL, +libxcursor: ?LibXCursor, +width: c_int, +height: c_int, +empty_event_pipe: [2]std.os.fd_t, +gl_ctx: ?*LibGL.Context, +wm_protocols: c.Atom, +wm_delete_window: c.Atom, +net_wm_ping: c.Atom, +net_wm_state_fullscreen: c.Atom, +net_wm_state: c.Atom, +net_wm_state_above: c.Atom, +net_wm_bypass_compositor: c.Atom, +motif_wm_hints: c.Atom, +net_wm_window_type: c.Atom, +net_wm_window_type_dock: c.Atom, +root_window: c.Window, +frame: *Frequency, +input: *Frequency, +window: c.Window, +backend_type: gpu.BackendType, +user_ptr: UserPtr, +instance: *gpu.Instance, +surface: *gpu.Surface, +gpu_adapter: *gpu.Adapter, +gpu_device: *gpu.Device, +refresh_rate: u32, +hidden_cursor: c.Cursor, + +// Mutable fields only used by main thread +app_update_thread_started: bool = false, +linux_gamemode: ?bool = null, +cursors: [@typeInfo(CursorShape).Enum.fields.len]?c.Cursor, +last_windowed_size: mach_core.Size, + +// Event queue; written from main thread; read from any +events_mu: std.Thread.RwLock = .{}, +events: EventQueue, + +// Input state; written from main thread; read from any +input_mu: std.Thread.RwLock = .{}, +input_state: InputState = .{}, +present_joysticks: std.StaticBitSet(@typeInfo(glfw.Joystick.Id).Enum.fields.len), + +// Signals to the App.update thread to do something +swap_chain_update: std.Thread.ResetEvent = .{}, +state_update: std.Thread.ResetEvent = .{}, +done: std.Thread.ResetEvent = .{}, +oom: std.Thread.ResetEvent = .{}, + +// Mutable fields; written by the App.update thread, read from any +swap_chain_mu: std.Thread.RwLock = .{}, +swap_chain_desc: gpu.SwapChain.Descriptor, +swap_chain: *gpu.SwapChain, + +// Mutable state fields; read/write by any thread +state_mu: std.Thread.Mutex = .{}, +current_title: [:0]const u8, +current_title_changed: bool = false, +current_display_mode: DisplayMode = .windowed, +current_vsync_mode: VSyncMode = .triple, +last_display_mode: DisplayMode = .windowed, +last_vsync_mode: VSyncMode = .triple, +current_border: bool, +last_border: bool, +current_headless: bool, +last_headless: bool, +current_size: Size, +last_size: Size, +current_size_limit: SizeLimit = .{ + .min = .{ .width = 350, .height = 350 }, + .max = .{ .width = null, .height = null }, +}, +last_size_limit: SizeLimit = .{ .min = .{}, .max = .{} }, +current_cursor_mode: CursorMode = .normal, +last_cursor_mode: CursorMode = .normal, +current_cursor_shape: CursorShape = .arrow, +last_cursor_shape: CursorShape = .arrow, + +const EventQueue = std.fifo.LinearFifo(Event, .Dynamic); + +pub const EventIterator = struct { + events_mu: *std.Thread.RwLock, + queue: *EventQueue, + + pub inline fn next(self: *EventIterator) ?Event { + self.events_mu.lockShared(); + defer self.events_mu.unlockShared(); + return self.queue.readItem(); + } +}; + +const UserPtr = struct { + self: *Core, +}; + +// TODO(important): expose device loss to users, this can happen especially in the web and on mobile +// devices. Users will need to re-upload all assets to the GPU in this event. +fn deviceLostCallback(reason: gpu.Device.LostReason, msg: [*:0]const u8, userdata: ?*anyopaque) callconv(.C) void { + _ = userdata; + _ = reason; + log.err("mach: device lost: {s}", .{msg}); + @panic("mach: device lost"); +} + +// Called on the main thread +pub fn init( + core: *Core, + allocator: std.mem.Allocator, + frame: *Frequency, + input: *Frequency, + options: Options, +) !void { + if (!@import("builtin").is_test and mach_core.options.use_wgpu) _ = mach_core.wgpu.Export(@import("root").GPUInterface); + if (!@import("builtin").is_test and mach_core.options.use_sysgpu) _ = mach_core.sysgpu.sysgpu.Export(@import("root").SYSGPUInterface); + + const libx11 = try LibX11.load(); + const libxcursor: ?LibXCursor = LibXCursor.load() catch |err| switch (err) { + error.LibraryNotFound => null, + else => return err, + }; + const libxrr: ?LibXRR = LibXRR.load() catch |err| switch (err) { + error.LibraryNotFound => null, + else => return err, + }; + const libgl: ?LibGL = LibGL.load() catch |err| switch (err) { + error.LibraryNotFound => null, + else => return err, + }; + + _ = libx11.XSetErrorHandler(errorHandler); + _ = libx11.XInitThreads(); + _ = libx11.XrmInitialize(); + + const display = libx11.XOpenDisplay(null) orelse { + std.log.err("X11: Cannot open display", .{}); + return error.CannotOpenDisplay; + }; + + const screen = c.DefaultScreen(display); + const root_window = c.RootWindow(display, screen); + const visual = c.DefaultVisual(display, screen); + const colormap = libx11.XCreateColormap(display, root_window, visual, c.AllocNone); + var set_window_attrs = c.XSetWindowAttributes{ + .colormap = colormap, + // TODO: reduce + .event_mask = c.StructureNotifyMask | c.KeyPressMask | c.KeyReleaseMask | + c.PointerMotionMask | c.ButtonPressMask | c.ButtonReleaseMask | + c.ExposureMask | c.FocusChangeMask | c.VisibilityChangeMask | + c.EnterWindowMask | c.LeaveWindowMask | c.PropertyChangeMask, + }; + defer _ = libx11.XFreeColormap(display, colormap); + + const empty_event_pipe = try std.os.pipe(); + for (0..2) |i| { + const sf = try std.os.fcntl(empty_event_pipe[i], std.os.F.GETFL, 0); + const df = try std.os.fcntl(empty_event_pipe[i], std.os.F.GETFD, 0); + _ = try std.os.fcntl(empty_event_pipe[i], std.os.F.SETFL, sf | std.os.O.NONBLOCK); + _ = try std.os.fcntl(empty_event_pipe[i], std.os.F.SETFD, df | std.os.FD_CLOEXEC); + } + + const window = libx11.XCreateWindow( + display, + root_window, + @divFloor(libx11.XDisplayWidth(display, screen), 2), // TODO: add window width? + @divFloor(libx11.XDisplayHeight(display, screen), 2), // TODO: add window height? + options.size.width, + options.size.height, + 0, + c.DefaultDepth(display, screen), + c.InputOutput, + visual, + c.CWColormap | c.CWEventMask, + &set_window_attrs, + ); + + const wm_protocols = libx11.XInternAtom(display, "WM_PROTOCOLS", c.False); + const wm_delete_window = libx11.XInternAtom(display, "WM_DELETE_WINDOW", c.False); + const net_wm_ping = libx11.XInternAtom(display, "NET_WM_PING", c.False); + const net_wm_state_fullscreen = libx11.XInternAtom(display, "_NET_WM_STATE_FULLSCREEN", c.False); + const net_wm_state = libx11.XInternAtom(display, "_NET_WM_STATE", c.False); + const net_wm_state_above = libx11.XInternAtom(display, "_NET_WM_STATE_ABOVE", c.False); + const motif_wm_hints = libx11.XInternAtom(display, "_MOTIF_WM_HINTS", c.False); + const net_wm_window_type = libx11.XInternAtom(display, "_NET_WM_WINDOW_TYPE", c.False); + const net_wm_window_type_dock = libx11.XInternAtom(display, "_NET_WM_WINDOW_TYPE_DOCK", c.False); + const net_wm_bypass_compositor = libx11.XInternAtom(display, "_NET_WM_BYPASS_COMPOSITOR", c.False); + + var protocols = [_]c.Atom{ wm_delete_window, net_wm_ping }; + _ = libx11.XSetWMProtocols(display, window, &protocols, protocols.len); + + _ = libx11.XStoreName(display, window, options.title.ptr); + _ = libx11.XSelectInput(display, window, set_window_attrs.event_mask); + _ = libx11.XMapWindow(display, window); + + var window_attrs: c.XWindowAttributes = undefined; + _ = libx11.XGetWindowAttributes(display, window, &window_attrs); + + const backend_type = try detectBackendType(allocator); + + const refresh_rate: u16 = blk: { + if (libxrr != null) { + const conf = libxrr.?.XRRGetScreenInfo(display, root_window); + break :blk @intCast(libxrr.?.XRRConfigCurrentRate(conf)); + } + break :blk 60; + }; + frame.target = 2 * refresh_rate; + + var gl_ctx: ?*LibGL.Context = null; + switch (backend_type) { + .opengl, .opengles => { + if (libgl != null) { + // zig fmt: off + const attrs = &[_]c_int{ + LibGL.rgba, + LibGL.doublebuffer, + LibGL.depth_size, 24, + LibGL.stencil_size, 8, + LibGL.red_size, 8, + LibGL.green_size, 8, + LibGL.blue_size, 8, + LibGL.sample_buffers, 0, + LibGL.samples, 0, + c.None, + }; + // zig fmt: on + + const visual_info = libgl.?.glXChooseVisual(display, screen, attrs.ptr); + defer _ = libx11.XFree(visual_info); + gl_ctx = libgl.?.glXCreateContext(display, visual_info, null, true); + _ = libgl.?.glXMakeCurrent(display, window, gl_ctx); + } else { + return error.LibGLNotFound; + } + }, + else => {}, + } + + const instance = gpu.createInstance(null) orelse { + log.err("failed to create GPU instance", .{}); + std.process.exit(1); + }; + const surface = instance.createSurface(&gpu.Surface.Descriptor{ + .next_in_chain = .{ + .from_xlib_window = &.{ + .display = display, + .window = @intCast(window), + }, + }, + }); + + var response: RequestAdapterResponse = undefined; + instance.requestAdapter(&gpu.RequestAdapterOptions{ + .compatible_surface = surface, + .power_preference = options.power_preference, + .force_fallback_adapter = .false, + }, &response, requestAdapterCallback); + if (response.status != .success) { + log.err("failed to create GPU adapter: {?s}", .{response.message}); + log.info("-> maybe try MACH_GPU_BACKEND=opengl ?", .{}); + std.process.exit(1); + } + + // Print which adapter we are going to use. + var props = std.mem.zeroes(gpu.Adapter.Properties); + response.adapter.?.getProperties(&props); + if (props.backend_type == .null) { + log.err("no backend found for {s} adapter", .{props.adapter_type.name()}); + std.process.exit(1); + } + log.info("found {s} backend on {s} adapter: {s}, {s}\n", .{ + props.backend_type.name(), + props.adapter_type.name(), + props.name, + props.driver_description, + }); + + // Create a device with default limits/features. + const gpu_device = response.adapter.?.createDevice(&.{ + .next_in_chain = .{ + .dawn_toggles_descriptor = &gpu.dawn.TogglesDescriptor.init(.{ + .enabled_toggles = &[_][*:0]const u8{ + "allow_unsafe_apis", + }, + }), + }, + + .required_features_count = if (options.required_features) |v| @as(u32, @intCast(v.len)) else 0, + .required_features = if (options.required_features) |v| @as(?[*]const gpu.FeatureName, v.ptr) else null, + .required_limits = if (options.required_limits) |limits| @as(?*const gpu.RequiredLimits, &gpu.RequiredLimits{ + .limits = limits, + }) else null, + .device_lost_callback = &deviceLostCallback, + .device_lost_userdata = null, + }) orelse { + log.err("failed to create GPU device\n", .{}); + std.process.exit(1); + }; + gpu_device.setUncapturedErrorCallback({}, printUnhandledErrorCallback); + + const swap_chain_desc = gpu.SwapChain.Descriptor{ + .label = "main swap chain", + .usage = .{ .render_attachment = true }, + .format = .bgra8_unorm, + .width = @intCast(window_attrs.width), + .height = @intCast(window_attrs.height), + .present_mode = .mailbox, + }; + const swap_chain = gpu_device.createSwapChain(surface, &swap_chain_desc); + + mach_core.adapter = response.adapter.?; + mach_core.device = gpu_device; + mach_core.queue = gpu_device.getQueue(); + mach_core.swap_chain = swap_chain; + mach_core.descriptor = swap_chain_desc; + + // The initial capacity we choose for the event queue is 2x our maximum expected event rate per + // frame. Specifically, 1000hz mouse updates are likely the maximum event rate we will encounter + // so we anticipate 2x that. If the event rate is higher than this per frame, it will grow to + // that maximum (we never shrink the event queue capacity in order to avoid allocations causing + // any stutter.) + var events = EventQueue.init(allocator); + try events.ensureTotalCapacity(2048); + + const window_size = mach_core.Size{ .width = @intCast(window_attrs.width), .height = @intCast(window_attrs.height) }; + + // Create hidden cursor + const blank_pixmap = libx11.XCreatePixmap(display, window, 1, 1, 1); + const gc = libx11.XCreateGC(display, blank_pixmap, 0, null); + if (gc != null) { + _ = libx11.XDrawPoint(display, blank_pixmap, gc, 0, 0); + _ = libx11.XFreeGC(display, gc); + } + var color = c.XColor{}; + const hidden_cursor = libx11.XCreatePixmapCursor(display, blank_pixmap, blank_pixmap, &color, &color, 0, 0); + + core.* = .{ + .allocator = allocator, + .display = display, + .libx11 = libx11, + .libgl = libgl, + .libxcursor = libxcursor, + .libxrr = libxrr, + .empty_event_pipe = empty_event_pipe, + .gl_ctx = gl_ctx, + .width = window_attrs.width, + .height = window_attrs.height, + .wm_protocols = wm_protocols, + .wm_delete_window = wm_delete_window, + .net_wm_ping = net_wm_ping, + .net_wm_state_fullscreen = net_wm_state_fullscreen, + .net_wm_state = net_wm_state, + .net_wm_state_above = net_wm_state_above, + .net_wm_window_type = net_wm_window_type, + .net_wm_window_type_dock = net_wm_window_type_dock, + .net_wm_bypass_compositor = net_wm_bypass_compositor, + .motif_wm_hints = motif_wm_hints, + .root_window = root_window, + .frame = frame, + .input = input, + .window = window, + .hidden_cursor = hidden_cursor, + .backend_type = backend_type, + .user_ptr = .{ .self = core }, + .instance = instance, + .surface = surface, + .gpu_adapter = response.adapter.?, + .gpu_device = gpu_device, + .refresh_rate = refresh_rate, + .swap_chain = swap_chain, + .swap_chain_desc = swap_chain_desc, + .events = events, + .current_title = options.title, + .current_display_mode = options.display_mode, + .last_display_mode = .windowed, + .current_border = options.border, + .last_border = true, + .current_headless = options.headless, + .last_headless = options.headless, + .current_size = window_size, + .last_size = window_size, + .last_windowed_size = window_size, + .cursors = std.mem.zeroes([@typeInfo(CursorShape).Enum.fields.len]?c.Cursor), + .present_joysticks = std.StaticBitSet(@typeInfo(glfw.Joystick.Id).Enum.fields.len).initEmpty(), + }; + core.cursors[@intFromEnum(CursorShape.arrow)] = try core.createStandardCursor(.arrow); + + core.state_update.set(); + try core.input.start(); + + if (builtin.os.tag == .linux and !options.is_app and + core.linux_gamemode == null and try wantGamemode(core.allocator)) + core.linux_gamemode = initLinuxGamemode(); +} + +// const joystick_callback = struct { +// fn callback(joystick: glfw.Joystick, event: glfw.Joystick.Event) void { +// const idx: u8 = @intCast(@intFromEnum(joystick.jid)); + +// switch (event) { +// .connected => { +// pf.input_mu.lock(); +// pf.present_joysticks.set(idx); +// pf.input_mu.unlock(); +// pf.pushEvent(.{ +// .joystick_connected = @enumFromInt(idx), +// }); +// }, +// .disconnected => { +// pf.input_mu.lock(); +// pf.present_joysticks.unset(idx); +// pf.input_mu.unlock(); +// pf.pushEvent(.{ +// .joystick_disconnected = @enumFromInt(idx), +// }); +// }, +// } +// } +// }.callback; +// glfw.Joystick.setCallback(joystick_callback); + +fn pushEvent(self: *Core, event: Event) void { + self.events_mu.lock(); + defer self.events_mu.unlock(); + self.events.writeItem(event) catch self.oom.set(); +} + +// Called on the main thread +pub fn deinit(self: *Core) void { + for (self.cursors) |cur| { + if (cur) |_| { + // _ = self.libx11.XFreeCursor(self.display, cur.?); + } + } + self.events.deinit(); + + if (builtin.os.tag == .linux and + self.linux_gamemode != null and + self.linux_gamemode.?) + deinitLinuxGamemode(); + + self.gpu_device.setDeviceLostCallback(null, null); + + self.swap_chain.release(); + self.surface.release(); + mach_core.queue.release(); + self.gpu_device.release(); + self.gpu_adapter.release(); + self.instance.release(); + + if (self.libxcursor) |*libxcursor| { + libxcursor.handle.close(); + } + + if (self.libxrr) |*libxrr| { + libxrr.handle.close(); + } + + if (self.libgl) |*libgl| { + if (self.gl_ctx) |gl_ctx| { + libgl.glXDestroyContext(self.display, gl_ctx); + } + libgl.handle.close(); + } + + _ = self.libx11.XUnmapWindow(self.display, self.window); + _ = self.libx11.XDestroyWindow(self.display, self.window); + _ = self.libx11.XCloseDisplay(self.display); + self.libx11.handle.close(); + + std.os.close(self.empty_event_pipe[0]); + std.os.close(self.empty_event_pipe[1]); +} + +// Secondary app-update thread +pub fn appUpdateThread(self: *Core, app: anytype) void { + self.frame.start() catch unreachable; + while (true) { + if (self.swap_chain_update.isSet()) blk: { + self.swap_chain_update.reset(); + + if (self.current_vsync_mode != self.last_vsync_mode) { + self.last_vsync_mode = self.current_vsync_mode; + switch (self.current_vsync_mode) { + .triple => self.frame.target = 2 * self.refresh_rate, + else => self.frame.target = 0, + } + } + + if (self.current_size.width == 0 or self.current_size.height == 0) break :blk; + + self.swap_chain_mu.lock(); + defer self.swap_chain_mu.unlock(); + mach_core.swap_chain.release(); + self.swap_chain_desc.width = self.current_size.width; + self.swap_chain_desc.height = self.current_size.height; + self.swap_chain = self.gpu_device.createSwapChain(self.surface, &self.swap_chain_desc); + + mach_core.swap_chain = self.swap_chain; + mach_core.descriptor = self.swap_chain_desc; + + self.pushEvent(.{ + .framebuffer_resize = .{ + .width = self.current_size.width, + .height = self.current_size.height, + }, + }); + } + + if (app.update() catch unreachable) { + self.done.set(); + + // Wake the main thread from any event handling, so there is not e.g. a one second delay + // in exiting the application. + self.wakeMainThread(); + return; + } + self.gpu_device.tick(); + self.gpu_device.machWaitForCommandsToBeScheduled(); + + self.frame.tick(); + if (self.frame.delay_ns != 0) std.time.sleep(self.frame.delay_ns); + } +} + +// Called on the main thread +pub fn update(self: *Core, app: anytype) !bool { + if (self.done.isSet()) return true; + if (!self.app_update_thread_started) { + self.app_update_thread_started = true; + const thread = try std.Thread.spawn(.{}, appUpdateThread, .{ self, app }); + thread.detach(); + } + + while (c.QLength(self.display) != 0) { + var event: c.XEvent = undefined; + _ = self.libx11.XNextEvent(self.display, &event); + self.processEvent(&event); + } + _ = self.libx11.XFlush(self.display); + + if (self.state_update.isSet()) { + self.state_update.reset(); + + // Title changes + if (self.current_title_changed) { + self.current_title_changed = false; + _ = self.libx11.XStoreName(self.display, self.window, self.current_title.ptr); + } + + // Display mode changes + if (self.current_display_mode != self.last_display_mode) { + switch (self.current_display_mode) { + .windowed => { + var atoms = std.BoundedArray(c.Atom, 5){}; + + if (self.last_display_mode == .fullscreen) { + atoms.append(self.net_wm_state_fullscreen) catch unreachable; + } + + atoms.append(self.motif_wm_hints) catch unreachable; + + // TODO + // if (self.floating) { + // atoms.append(self.net_wm_state_above) catch unreachable; + // } + _ = self.libx11.XChangeProperty( + self.display, + self.window, + self.net_wm_state, + c.XA_ATOM, + 32, + c.PropModeReplace, + @ptrCast(atoms.slice()), + atoms.len, + ); + + self.setFullscreen(false); + self.setDecorated(self.current_border); + self.setFloating(false); + _ = self.libx11.XMapWindow(self.display, self.window); + _ = self.libx11.XFlush(self.display); + }, + .fullscreen => { + if (self.last_display_mode == .windowed) { + self.last_windowed_size = self.current_size; + } + + self.setFullscreen(true); + _ = self.libx11.XFlush(self.display); + }, + .borderless => { + if (self.last_display_mode == .windowed) { + self.last_windowed_size = self.current_size; + } + + self.setDecorated(false); + self.setFloating(true); + self.setFullscreen(false); + + _ = self.libx11.XResizeWindow( + self.display, + self.window, + @intCast(c.DisplayWidth(self.display, c.DefaultScreen(self.display))), + @intCast(c.DisplayHeight(self.display, c.DefaultScreen(self.display))), + ); + _ = self.libx11.XFlush(self.display); + }, + } + + self.last_display_mode = self.current_display_mode; + } + + // Border changes + if (self.current_border != self.last_border) { + self.last_border = self.current_border; + // if (self.current_display_mode != .borderless) self.window.setAttrib(.decorated, self.current_border); + } + + // Headless changes + if (self.current_headless != self.last_headless) { + self.current_headless = self.last_headless; + // if (self.current_headless) self.window.hide() else self.window.show(); + } + + // Size changes + if (!self.current_size.eql(self.last_size)) { + self.last_size = self.current_size; + _ = self.libx11.XResizeWindow(self.display, self.window, self.current_size.width, self.current_size.height); + _ = self.libx11.XFlush(self.display); + } + + // Size limit changes + if (!self.current_size_limit.eql(self.last_size_limit)) { + self.last_size_limit = self.current_size_limit; + // self.window.setSizeLimits( + // .{ .width = self.current_size_limit.min.width, .height = self.current_size_limit.min.height }, + // .{ .width = self.current_size_limit.max.width, .height = self.current_size_limit.max.height }, + // ); + } + + // Cursor mode changes + if (self.current_cursor_mode != self.last_cursor_mode) { + self.updateCursor(); + self.last_cursor_mode = self.current_cursor_mode; + } + + // Cursor shape changes + if (self.current_cursor_shape != self.last_cursor_shape) { + self.last_cursor_shape = self.current_cursor_shape; + const cursor = self.createStandardCursor(self.current_cursor_shape) catch |err| blk: { + log.warn("mach: setCursorShape: {}: {s} not yet supported\n", .{ + err, + @tagName(self.current_cursor_shape), + }); + break :blk null; + }; + self.cursors[@intFromEnum(self.current_cursor_shape)] = cursor; + self.updateCursor(); + } + } + + // const frequency_delay = @as(f32, @floatFromInt(self.input.delay_ns)) / @as(f32, @floatFromInt(std.time.ns_per_s)); + // glfw.waitEventsTimeout(frequency_delay); + + if (@hasDecl(std.meta.Child(@TypeOf(app)), "updateMainThread")) { + if (app.updateMainThread() catch unreachable) { + self.done.set(); + return true; + } + } + + self.input.tick(); + return false; +} + +// May be called from any thread. +pub inline fn pollEvents(self: *Core) EventIterator { + return EventIterator{ .events_mu = &self.events_mu, .queue = &self.events }; +} + +// May be called from any thread. +pub fn setTitle(self: *Core, title: [:0]const u8) void { + self.state_mu.lock(); + defer self.state_mu.unlock(); + self.current_title = title; + self.current_title_changed = true; + self.state_update.set(); + self.wakeMainThread(); +} + +// May be called from any thread. +pub fn setDisplayMode(self: *Core, mode: DisplayMode) void { + self.state_mu.lock(); + defer self.state_mu.unlock(); + self.current_display_mode = mode; + if (self.current_display_mode != self.last_display_mode) { + self.state_update.set(); + self.wakeMainThread(); + } +} + +// May be called from any thread. +pub fn displayMode(self: *Core) DisplayMode { + self.state_mu.lock(); + defer self.state_mu.unlock(); + return self.current_display_mode; +} + +// May be called from any thread. +pub fn setBorder(self: *Core, value: bool) void { + self.state_mu.lock(); + defer self.state_mu.unlock(); + self.current_border = value; + if (self.current_border != self.last_border) { + self.state_update.set(); + self.wakeMainThread(); + } +} + +// May be called from any thread. +pub fn border(self: *Core) bool { + self.state_mu.lock(); + defer self.state_mu.unlock(); + return self.current_border; +} + +// May be called from any thread. +pub fn setHeadless(self: *Core, value: bool) void { + self.state_mu.lock(); + defer self.state_mu.unlock(); + self.current_headless = value; + if (self.current_headless != self.last_headless) { + self.state_update.set(); + self.wakeMainThread(); + } +} + +// May be called from any thread. +pub fn headless(self: *Core) bool { + self.state_mu.lock(); + defer self.state_mu.unlock(); + return self.current_headless; +} + +// May be called from any thread. +pub fn setVSync(self: *Core, mode: VSyncMode) void { + self.swap_chain_mu.lock(); + self.swap_chain_desc.present_mode = switch (mode) { + .none => .immediate, + .double => .fifo, + .triple => .mailbox, + }; + self.current_vsync_mode = mode; + self.swap_chain_mu.unlock(); + self.swap_chain_update.set(); + self.wakeMainThread(); +} + +// May be called from any thread. +pub fn vsync(self: *Core) VSyncMode { + self.swap_chain_mu.lockShared(); + defer self.swap_chain_mu.unlockShared(); + return switch (self.swap_chain_desc.present_mode) { + .immediate => .none, + .fifo => .double, + .mailbox => .triple, + }; +} + +// May be called from any thread. +pub fn setSize(self: *Core, value: Size) void { + self.state_mu.lock(); + defer self.state_mu.unlock(); + self.current_size = value; + if (!self.current_size.eql(self.last_size)) { + self.state_update.set(); + self.wakeMainThread(); + } +} + +// May be called from any thread. +pub fn size(self: *Core) Size { + self.state_mu.lock(); + defer self.state_mu.unlock(); + return self.current_size; +} + +// May be called from any thread. +pub fn setSizeLimit(self: *Core, limit: SizeLimit) void { + self.state_mu.lock(); + defer self.state_mu.unlock(); + self.current_size_limit = limit; + if (!self.current_size_limit.eql(self.last_size_limit)) { + self.state_update.set(); + self.wakeMainThread(); + } +} + +// May be called from any thread. +pub fn sizeLimit(self: *Core) SizeLimit { + self.state_mu.lock(); + defer self.state_mu.unlock(); + return self.current_size_limit; +} + +// May be called from any thread. +pub fn setCursorMode(self: *Core, mode: CursorMode) void { + self.state_mu.lock(); + defer self.state_mu.unlock(); + self.current_cursor_mode = mode; + if (self.current_cursor_mode != self.last_cursor_mode) { + self.state_update.set(); + self.wakeMainThread(); + } +} + +// May be called from any thread. +pub fn cursorMode(self: *Core) CursorMode { + self.state_mu.lock(); + defer self.state_mu.unlock(); + return self.current_cursor_mode; +} + +// May be called from any thread. +pub fn setCursorShape(self: *Core, shape: CursorShape) void { + self.state_mu.lock(); + defer self.state_mu.unlock(); + self.current_cursor_shape = shape; + if (self.current_cursor_shape != self.last_cursor_shape) { + self.state_update.set(); + self.wakeMainThread(); + } +} + +// May be called from any thread. +pub fn cursorShape(self: *Core) CursorShape { + self.state_mu.lock(); + defer self.state_mu.unlock(); + return self.current_cursor_shape; +} + +// May be called from any thread. +pub fn joystickPresent(_: *Core, _: Joystick) bool { + @panic("TODO: implement joystickPresent for X11"); +} + +// May be called from any thread. +pub fn joystickName(_: *Core, _: Joystick) ?[:0]const u8 { + @panic("TODO: implement joystickName for X11"); +} + +// May be called from any thread. +pub fn joystickButtons(_: *Core, _: Joystick) ?[]const bool { + @panic("TODO: implement joystickButtons for X11"); +} + +// May be called from any thread. +pub fn joystickAxes(_: *Core, _: Joystick) ?[]const f32 { + @panic("TODO: implement joystickAxes for X11"); +} + +// May be called from any thread. +pub fn keyPressed(self: *Core, key: Key) bool { + self.input_mu.lockShared(); + defer self.input_mu.unlockShared(); + return self.input_state.isKeyPressed(key); +} + +// May be called from any thread. +pub fn keyReleased(self: *Core, key: Key) bool { + self.input_mu.lockShared(); + defer self.input_mu.unlockShared(); + return self.input_state.isKeyReleased(key); +} + +// May be called from any thread. +pub fn mousePressed(self: *Core, button: MouseButton) bool { + self.input_mu.lockShared(); + defer self.input_mu.unlockShared(); + return self.input_state.isMouseButtonPressed(button); +} + +// May be called from any thread. +pub fn mouseReleased(self: *Core, button: MouseButton) bool { + self.input_mu.lockShared(); + defer self.input_mu.unlockShared(); + return self.input_state.isMouseButtonReleased(button); +} + +// May be called from any thread. +pub fn mousePosition(self: *Core) mach_core.Position { + self.input_mu.lockShared(); + defer self.input_mu.unlockShared(); + return self.input_state.mouse_position; +} + +// May be called from any thread. +pub inline fn outOfMemory(self: *Core) bool { + if (self.oom.isSet()) { + self.oom.reset(); + return true; + } + return false; +} + +// May be called from any thread. +pub inline fn wakeMainThread(self: *Core) void { + while (true) { + const result = std.os.write(self.empty_event_pipe[1], &.{0}) catch break; + if (result == 1) break; + } +} + +fn processEvent(self: *Core, event: *c.XEvent) void { + switch (event.type) { + c.KeyPress, c.KeyRelease => { + // TODO: key repeat event + + var keysym: c.KeySym = undefined; + _ = self.libx11.XLookupString(&event.xkey, null, 0, &keysym, null); + + const key_event = KeyEvent{ .key = toMachKey(keysym), .mods = toMachMods(event.xkey.state) }; + + switch (event.type) { + c.KeyPress => { + self.input_mu.lock(); + self.input_state.keys.set(@intFromEnum(key_event.key)); + self.input_mu.unlock(); + self.pushEvent(.{ .key_press = key_event }); + + if (unicode.unicodeFromKeySym(keysym)) |codepoint| { + self.pushEvent(.{ .char_input = .{ .codepoint = codepoint } }); + } + }, + c.KeyRelease => { + self.input_mu.lock(); + self.input_state.keys.unset(@intFromEnum(key_event.key)); + self.input_mu.unlock(); + self.pushEvent(.{ .key_release = key_event }); + }, + else => unreachable, + } + }, + c.ButtonPress => { + const button = toMachButton(event.xbutton.button) orelse { + // Modern X provides scroll events as mouse button presses + const scroll: struct { f32, f32 } = switch (event.xbutton.button) { + c.Button4 => .{ 0.0, 1.0 }, + c.Button5 => .{ 0.0, -1.0 }, + 6 => .{ 1.0, 0.0 }, + 7 => .{ -1.0, 0.0 }, + else => unreachable, + }; + self.pushEvent(.{ .mouse_scroll = .{ .xoffset = scroll[0], .yoffset = scroll[1] } }); + return; + }; + const cursor_pos = self.getCursorPos(); + const mouse_button = MouseButtonEvent{ + .button = button, + .pos = cursor_pos, + .mods = toMachMods(event.xbutton.state), + }; + + self.input_mu.lock(); + self.input_state.mouse_buttons.set(@intFromEnum(mouse_button.button)); + self.input_mu.unlock(); + self.pushEvent(.{ .mouse_press = mouse_button }); + }, + c.ButtonRelease => { + const button = toMachButton(event.xbutton.button) orelse return; + const cursor_pos = self.getCursorPos(); + const mouse_button = MouseButtonEvent{ + .button = button, + .pos = cursor_pos, + .mods = toMachMods(event.xbutton.state), + }; + + self.input_mu.lock(); + self.input_state.mouse_buttons.unset(@intFromEnum(mouse_button.button)); + self.input_mu.unlock(); + self.pushEvent(.{ .mouse_release = mouse_button }); + }, + c.ClientMessage => { + if (event.xclient.message_type == c.None) return; + + if (event.xclient.message_type == self.wm_protocols) { + const protocol = event.xclient.data.l[0]; + if (protocol == c.None) return; + + if (protocol == self.wm_delete_window) { + self.pushEvent(.close); + } else if (protocol == self.net_wm_ping) { + // The window manager is pinging the application to ensure + // it's still responding to events + var reply = event.*; + reply.xclient.window = self.root_window; + _ = self.libx11.XSendEvent( + self.display, + self.root_window, + c.False, + c.SubstructureNotifyMask | c.SubstructureRedirectMask, + &reply, + ); + } + } + }, + c.EnterNotify => { + const x: f32 = @floatFromInt(event.xcrossing.x); + const y: f32 = @floatFromInt(event.xcrossing.y); + self.input_mu.lock(); + self.input_state.mouse_position = .{ .x = x, .y = y }; + self.input_mu.unlock(); + self.pushEvent(.{ .mouse_motion = .{ .pos = .{ .x = x, .y = y } } }); + }, + c.MotionNotify => { + const x: f32 = @floatFromInt(event.xmotion.x); + const y: f32 = @floatFromInt(event.xmotion.y); + self.input_mu.lock(); + self.input_state.mouse_position = .{ .x = x, .y = y }; + self.input_mu.unlock(); + self.pushEvent(.{ .mouse_motion = .{ .pos = .{ .x = x, .y = y } } }); + }, + c.ConfigureNotify => { + if (event.xconfigure.width != self.last_size.width or + event.xconfigure.height != self.last_size.height) + { + self.state_mu.lock(); + defer self.state_mu.unlock(); + self.current_size.width = @intCast(event.xconfigure.width); + self.current_size.height = @intCast(event.xconfigure.height); + self.swap_chain_update.set(); + } + }, + c.FocusIn => { + if (event.xfocus.mode == c.NotifyGrab or + event.xfocus.mode == c.NotifyUngrab) + { + // Ignore focus events from popup indicator windows, window menu + // key chords and window dragging + return; + } + + self.pushEvent(.focus_gained); + }, + c.FocusOut => { + if (event.xfocus.mode == c.NotifyGrab or + event.xfocus.mode == c.NotifyUngrab) + { + // Ignore focus events from popup indicator windows, window menu + // key chords and window dragging + return; + } + + self.pushEvent(.focus_lost); + }, + else => {}, + } +} + +fn setDecorated(self: *Core, enabled: bool) void { + const MWMHints = struct { + flags: u32, + functions: u32, + decorations: u32, + input_mode: i32, + status: u32, + }; + + const hints = MWMHints{ + .functions = 0, + .flags = 2, + .decorations = if (enabled) 1 else 0, + .input_mode = 0, + .status = 0, + }; + + _ = self.libx11.XChangeProperty( + self.display, + self.window, + self.motif_wm_hints, + self.motif_wm_hints, + 32, + c.PropModeReplace, + @ptrCast(&hints), + 5, + ); +} + +fn setFullscreen(self: *Core, enabled: bool) void { + self.sendEventToWM(self.net_wm_state, &.{ @intFromBool(enabled), @intCast(self.net_wm_state_fullscreen), 0, 1 }); + + // Force composition OFF to reduce overhead + const compositing_disable_on: c_long = @intFromBool(enabled); + const bypass_compositor = self.libx11.XInternAtom(self.display, "_NET_WM_BYPASS_COMPOSITOR", c.False); + + if (bypass_compositor != c.None) { + _ = self.libx11.XChangeProperty( + self.display, + self.window, + bypass_compositor, + c.XA_CARDINAL, + 32, + c.PropModeReplace, + @ptrCast(&compositing_disable_on), + 1, + ); + } +} + +fn setFloating(self: *Core, enabled: bool) void { + const net_wm_state_remove = 0; + const net_wm_state_add = 1; + const action: c_long = if (enabled) net_wm_state_add else net_wm_state_remove; + self.sendEventToWM(self.net_wm_state, &.{ action, @intCast(self.net_wm_state_above), 0, 1 }); +} + +fn sendEventToWM(self: *Core, message_type: c.Atom, data: []const c_long) void { + var ev = std.mem.zeroes(c.XEvent); + ev.type = c.ClientMessage; + ev.xclient.window = self.window; + ev.xclient.message_type = message_type; + ev.xclient.format = 32; + @memcpy(ev.xclient.data.l[0..data.len], data); + _ = self.libx11.XSendEvent( + self.display, + self.root_window, + c.False, + c.SubstructureNotifyMask | c.SubstructureRedirectMask, + &ev, + ); + _ = self.libx11.XFlush(self.display); +} + +fn getCursorPos(self: *Core) mach_core.Position { + var root_window: c.Window = undefined; + var child_window: c.Window = undefined; + var root_cursor_x: c_int = 0; + var root_cursor_y: c_int = 0; + var cursor_x: c_int = 0; + var cursor_y: c_int = 0; + var mask: c_uint = 0; + _ = self.libx11.XQueryPointer( + self.display, + self.window, + &root_window, + &child_window, + &root_cursor_x, + &root_cursor_y, + &cursor_x, + &cursor_y, + &mask, + ); + + return .{ .x = @floatFromInt(cursor_x), .y = @floatFromInt(cursor_y) }; +} + +// fn createImageCursor(display: *c.Display, pixels: []const u8, width: u32, height: u32) c.Cursor { +// const image = libxcursor.XcursorImageCreate(@intCast(width), @intCast(height)) orelse return c.None; +// defer libxcursor.XcursorImageDestroy(image); + +// for (image.*.pixels[0 .. width * height], 0..) |*target, i| { +// const r = pixels[i * 4 + 0]; +// const g = pixels[i * 4 + 1]; +// const b = pixels[i * 4 + 2]; +// const a: u32 = pixels[i * 4 + 3]; +// target.* = (a << 24) | +// ((r * a / 255) << 16) | +// ((g * a / 255) << 8) | +// ((b * a / 255) << 0); +// } + +// return libxcursor.XcursorImageLoadCursor(display, image); +// } + +fn updateCursor(self: *Core) void { + switch (self.current_cursor_mode) { + .normal => { + if (self.cursors[@intFromEnum(self.current_cursor_shape)]) |current_cursor| { + _ = self.libx11.XDefineCursor(self.display, self.window, current_cursor); + } else { + // TODO: what's the correct behavior here? reset to parent cursor? + _ = self.libx11.XUndefineCursor(self.display, self.window); + } + + if (self.last_cursor_mode == .disabled) { + _ = self.libx11.XUngrabPointer(self.display, c.CurrentTime); + } + }, + .hidden => { + _ = self.libx11.XDefineCursor(self.display, self.window, self.hidden_cursor); + if (self.last_cursor_mode == .disabled) { + _ = self.libx11.XUngrabPointer(self.display, c.CurrentTime); + } + }, + .disabled => { + _ = self.libx11.XDefineCursor(self.display, self.window, self.hidden_cursor); + _ = self.libx11.XGrabPointer( + self.display, + self.window, + c.True, + c.ButtonPressMask | c.ButtonReleaseMask | c.PointerMotionMask, + c.GrabModeAsync, + c.GrabModeAsync, + self.window, + c.None, + c.CurrentTime, + ); + }, + } +} + +fn createStandardCursor(self: *Core, shape: CursorShape) !c.Cursor { + if (self.libxcursor) |libxcursor| { + const theme = libxcursor.XcursorGetTheme(self.display); + if (theme != null) { + const name = switch (shape) { + .arrow => "default", + .ibeam => "text", + .crosshair => "crosshair", + .pointing_hand => "pointer", + .resize_ew => "ew-resize", + .resize_ns => "ns-resize", + .resize_nwse => "nwse-resize", + .resize_nesw => "nesw-resize", + .resize_all => "all-scroll", + .not_allowed => "not-allowed", + }; + + const cursor_size = libxcursor.XcursorGetDefaultSize(self.display); + const image = libxcursor.XcursorLibraryLoadImage(name, theme, cursor_size); + defer libxcursor.XcursorImageDestroy(image); + + if (image != null) { + return libxcursor.XcursorImageLoadCursor(self.display, image); + } + } + } + + const xc: c_uint = switch (shape) { + .arrow => c.XC_left_ptr, + .ibeam => c.XC_xterm, + .crosshair => c.XC_crosshair, + .pointing_hand => c.XC_hand2, + .resize_ew => c.XC_sb_h_double_arrow, + .resize_ns => c.XC_sb_v_double_arrow, + .resize_nwse => c.XC_sb_h_double_arrow, + .resize_nesw => c.XC_sb_h_double_arrow, + .resize_all => c.XC_fleur, + .not_allowed => c.XC_X_cursor, + }; + + const cursor = self.libx11.XCreateFontCursor(self.display, xc); + if (cursor == 0) return error.FailedToCreateCursor; + + return cursor; +} + +fn toMachButton(button: c_uint) ?MouseButton { + return switch (button) { + c.Button1 => .left, + c.Button2 => .middle, + c.Button3 => .right, + // Scroll events are handled by caller + c.Button4, c.Button5, 6, 7 => null, + // Additional buttons after 7 are treated as regular buttons + 8 => .four, + 9 => .five, + 10 => .six, + 11 => .seven, + 12 => .eight, + // Unknown button + else => null, + }; +} + +fn toMachKey(key: c.KeySym) Key { + return switch (key) { + c.XK_a, c.XK_A => .a, + c.XK_b, c.XK_B => .b, + c.XK_c, c.XK_C => .c, + c.XK_d, c.XK_D => .d, + c.XK_e, c.XK_E => .e, + c.XK_f, c.XK_F => .f, + c.XK_g, c.XK_G => .g, + c.XK_h, c.XK_H => .h, + c.XK_i, c.XK_I => .i, + c.XK_j, c.XK_J => .j, + c.XK_k, c.XK_K => .k, + c.XK_l, c.XK_L => .l, + c.XK_m, c.XK_M => .m, + c.XK_n, c.XK_N => .n, + c.XK_o, c.XK_O => .o, + c.XK_p, c.XK_P => .p, + c.XK_q, c.XK_Q => .q, + c.XK_r, c.XK_R => .r, + c.XK_s, c.XK_S => .s, + c.XK_t, c.XK_T => .t, + c.XK_u, c.XK_U => .u, + c.XK_v, c.XK_V => .v, + c.XK_w, c.XK_W => .w, + c.XK_x, c.XK_X => .x, + c.XK_y, c.XK_Y => .y, + c.XK_z, c.XK_Z => .z, + + c.XK_0 => .zero, + c.XK_1 => .one, + c.XK_2 => .two, + c.XK_3 => .three, + c.XK_4 => .four, + c.XK_5 => .five, + c.XK_6 => .six, + c.XK_7 => .seven, + c.XK_8 => .eight, + c.XK_9 => .nine, + + c.XK_F1 => .f1, + c.XK_F2 => .f2, + c.XK_F3 => .f3, + c.XK_F4 => .f4, + c.XK_F5 => .f5, + c.XK_F6 => .f6, + c.XK_F7 => .f7, + c.XK_F8 => .f8, + c.XK_F9 => .f9, + c.XK_F10 => .f10, + c.XK_F11 => .f11, + c.XK_F12 => .f12, + c.XK_F13 => .f13, + c.XK_F14 => .f14, + c.XK_F15 => .f15, + c.XK_F16 => .f16, + c.XK_F17 => .f17, + c.XK_F18 => .f18, + c.XK_F19 => .f19, + c.XK_F20 => .f20, + c.XK_F21 => .f21, + c.XK_F22 => .f22, + c.XK_F23 => .f23, + c.XK_F24 => .f24, + c.XK_F25 => .f25, + + c.XK_KP_Divide => .kp_divide, + c.XK_KP_Multiply => .kp_multiply, + c.XK_KP_Subtract => .kp_subtract, + c.XK_KP_Add => .kp_add, + c.XK_KP_0 => .kp_0, + c.XK_KP_1 => .kp_1, + c.XK_KP_2 => .kp_2, + c.XK_KP_3 => .kp_3, + c.XK_KP_4 => .kp_4, + c.XK_KP_5 => .kp_5, + c.XK_KP_6 => .kp_6, + c.XK_KP_7 => .kp_7, + c.XK_KP_8 => .kp_8, + c.XK_KP_9 => .kp_9, + c.XK_KP_Decimal => .kp_decimal, + c.XK_KP_Equal => .kp_equal, + c.XK_KP_Enter => .kp_enter, + + c.XK_Return => .enter, + c.XK_Escape => .escape, + c.XK_Tab => .tab, + c.XK_Shift_L => .left_shift, + c.XK_Shift_R => .right_shift, + c.XK_Control_L => .left_control, + c.XK_Control_R => .right_control, + c.XK_Alt_L => .left_alt, + c.XK_Alt_R => .right_alt, + c.XK_Super_L => .left_super, + c.XK_Super_R => .right_super, + c.XK_Menu => .menu, + c.XK_Num_Lock => .num_lock, + c.XK_Caps_Lock => .caps_lock, + c.XK_Print => .print, + c.XK_Scroll_Lock => .scroll_lock, + c.XK_Pause => .pause, + c.XK_Delete => .delete, + c.XK_Home => .home, + c.XK_End => .end, + c.XK_Page_Up => .page_up, + c.XK_Page_Down => .page_down, + c.XK_Insert => .insert, + c.XK_Left => .left, + c.XK_Right => .right, + c.XK_Up => .up, + c.XK_Down => .down, + c.XK_BackSpace => .backspace, + c.XK_space => .space, + c.XK_minus => .minus, + c.XK_equal => .equal, + c.XK_braceleft => .left_bracket, + c.XK_braceright => .right_bracket, + c.XK_backslash => .backslash, + c.XK_semicolon => .semicolon, + c.XK_apostrophe => .apostrophe, + c.XK_comma => .comma, + c.XK_period => .period, + c.XK_slash => .slash, + c.XK_grave => .grave, + + else => .unknown, + }; +} + +fn toMachMods(mods: c_uint) KeyMods { + return .{ + .shift = mods & c.ShiftMask != 0, + .control = mods & c.ControlMask != 0, + .alt = mods & c.Mod1Mask != 0, + .super = mods & c.Mod4Mask != 0, + .caps_lock = mods & c.LockMask != 0, + .num_lock = mods & c.Mod2Mask != 0, + }; +} + +fn errorHandler(display: ?*c.Display, event: [*c]c.XErrorEvent) callconv(.C) c_int { + _ = display; + log.err("X11: error code {d}\n", .{event.*.error_code}); + return 0; +} diff --git a/src/core/platform/x11/unicode.zig b/src/core/platform/x11/unicode.zig new file mode 100644 index 00000000..9f2d5348 --- /dev/null +++ b/src/core/platform/x11/unicode.zig @@ -0,0 +1,865 @@ +///! This code is taken from https://github.com/glfw/glfw/blob/master/src/xkb_unicode.c +const c = @import("Core.zig").c; + +const keysym_table = &[_]struct { c.KeySym, u21 }{ + .{ 0x01a1, 0x0104 }, + .{ 0x01a2, 0x02d8 }, + .{ 0x01a3, 0x0141 }, + .{ 0x01a5, 0x013d }, + .{ 0x01a6, 0x015a }, + .{ 0x01a9, 0x0160 }, + .{ 0x01aa, 0x015e }, + .{ 0x01ab, 0x0164 }, + .{ 0x01ac, 0x0179 }, + .{ 0x01ae, 0x017d }, + .{ 0x01af, 0x017b }, + .{ 0x01b1, 0x0105 }, + .{ 0x01b2, 0x02db }, + .{ 0x01b3, 0x0142 }, + .{ 0x01b5, 0x013e }, + .{ 0x01b6, 0x015b }, + .{ 0x01b7, 0x02c7 }, + .{ 0x01b9, 0x0161 }, + .{ 0x01ba, 0x015f }, + .{ 0x01bb, 0x0165 }, + .{ 0x01bc, 0x017a }, + .{ 0x01bd, 0x02dd }, + .{ 0x01be, 0x017e }, + .{ 0x01bf, 0x017c }, + .{ 0x01c0, 0x0154 }, + .{ 0x01c3, 0x0102 }, + .{ 0x01c5, 0x0139 }, + .{ 0x01c6, 0x0106 }, + .{ 0x01c8, 0x010c }, + .{ 0x01ca, 0x0118 }, + .{ 0x01cc, 0x011a }, + .{ 0x01cf, 0x010e }, + .{ 0x01d0, 0x0110 }, + .{ 0x01d1, 0x0143 }, + .{ 0x01d2, 0x0147 }, + .{ 0x01d5, 0x0150 }, + .{ 0x01d8, 0x0158 }, + .{ 0x01d9, 0x016e }, + .{ 0x01db, 0x0170 }, + .{ 0x01de, 0x0162 }, + .{ 0x01e0, 0x0155 }, + .{ 0x01e3, 0x0103 }, + .{ 0x01e5, 0x013a }, + .{ 0x01e6, 0x0107 }, + .{ 0x01e8, 0x010d }, + .{ 0x01ea, 0x0119 }, + .{ 0x01ec, 0x011b }, + .{ 0x01ef, 0x010f }, + .{ 0x01f0, 0x0111 }, + .{ 0x01f1, 0x0144 }, + .{ 0x01f2, 0x0148 }, + .{ 0x01f5, 0x0151 }, + .{ 0x01f8, 0x0159 }, + .{ 0x01f9, 0x016f }, + .{ 0x01fb, 0x0171 }, + .{ 0x01fe, 0x0163 }, + .{ 0x01ff, 0x02d9 }, + .{ 0x02a1, 0x0126 }, + .{ 0x02a6, 0x0124 }, + .{ 0x02a9, 0x0130 }, + .{ 0x02ab, 0x011e }, + .{ 0x02ac, 0x0134 }, + .{ 0x02b1, 0x0127 }, + .{ 0x02b6, 0x0125 }, + .{ 0x02b9, 0x0131 }, + .{ 0x02bb, 0x011f }, + .{ 0x02bc, 0x0135 }, + .{ 0x02c5, 0x010a }, + .{ 0x02c6, 0x0108 }, + .{ 0x02d5, 0x0120 }, + .{ 0x02d8, 0x011c }, + .{ 0x02dd, 0x016c }, + .{ 0x02de, 0x015c }, + .{ 0x02e5, 0x010b }, + .{ 0x02e6, 0x0109 }, + .{ 0x02f5, 0x0121 }, + .{ 0x02f8, 0x011d }, + .{ 0x02fd, 0x016d }, + .{ 0x02fe, 0x015d }, + .{ 0x03a2, 0x0138 }, + .{ 0x03a3, 0x0156 }, + .{ 0x03a5, 0x0128 }, + .{ 0x03a6, 0x013b }, + .{ 0x03aa, 0x0112 }, + .{ 0x03ab, 0x0122 }, + .{ 0x03ac, 0x0166 }, + .{ 0x03b3, 0x0157 }, + .{ 0x03b5, 0x0129 }, + .{ 0x03b6, 0x013c }, + .{ 0x03ba, 0x0113 }, + .{ 0x03bb, 0x0123 }, + .{ 0x03bc, 0x0167 }, + .{ 0x03bd, 0x014a }, + .{ 0x03bf, 0x014b }, + .{ 0x03c0, 0x0100 }, + .{ 0x03c7, 0x012e }, + .{ 0x03cc, 0x0116 }, + .{ 0x03cf, 0x012a }, + .{ 0x03d1, 0x0145 }, + .{ 0x03d2, 0x014c }, + .{ 0x03d3, 0x0136 }, + .{ 0x03d9, 0x0172 }, + .{ 0x03dd, 0x0168 }, + .{ 0x03de, 0x016a }, + .{ 0x03e0, 0x0101 }, + .{ 0x03e7, 0x012f }, + .{ 0x03ec, 0x0117 }, + .{ 0x03ef, 0x012b }, + .{ 0x03f1, 0x0146 }, + .{ 0x03f2, 0x014d }, + .{ 0x03f3, 0x0137 }, + .{ 0x03f9, 0x0173 }, + .{ 0x03fd, 0x0169 }, + .{ 0x03fe, 0x016b }, + .{ 0x047e, 0x203e }, + .{ 0x04a1, 0x3002 }, + .{ 0x04a2, 0x300c }, + .{ 0x04a3, 0x300d }, + .{ 0x04a4, 0x3001 }, + .{ 0x04a5, 0x30fb }, + .{ 0x04a6, 0x30f2 }, + .{ 0x04a7, 0x30a1 }, + .{ 0x04a8, 0x30a3 }, + .{ 0x04a9, 0x30a5 }, + .{ 0x04aa, 0x30a7 }, + .{ 0x04ab, 0x30a9 }, + .{ 0x04ac, 0x30e3 }, + .{ 0x04ad, 0x30e5 }, + .{ 0x04ae, 0x30e7 }, + .{ 0x04af, 0x30c3 }, + .{ 0x04b0, 0x30fc }, + .{ 0x04b1, 0x30a2 }, + .{ 0x04b2, 0x30a4 }, + .{ 0x04b3, 0x30a6 }, + .{ 0x04b4, 0x30a8 }, + .{ 0x04b5, 0x30aa }, + .{ 0x04b6, 0x30ab }, + .{ 0x04b7, 0x30ad }, + .{ 0x04b8, 0x30af }, + .{ 0x04b9, 0x30b1 }, + .{ 0x04ba, 0x30b3 }, + .{ 0x04bb, 0x30b5 }, + .{ 0x04bc, 0x30b7 }, + .{ 0x04bd, 0x30b9 }, + .{ 0x04be, 0x30bb }, + .{ 0x04bf, 0x30bd }, + .{ 0x04c0, 0x30bf }, + .{ 0x04c1, 0x30c1 }, + .{ 0x04c2, 0x30c4 }, + .{ 0x04c3, 0x30c6 }, + .{ 0x04c4, 0x30c8 }, + .{ 0x04c5, 0x30ca }, + .{ 0x04c6, 0x30cb }, + .{ 0x04c7, 0x30cc }, + .{ 0x04c8, 0x30cd }, + .{ 0x04c9, 0x30ce }, + .{ 0x04ca, 0x30cf }, + .{ 0x04cb, 0x30d2 }, + .{ 0x04cc, 0x30d5 }, + .{ 0x04cd, 0x30d8 }, + .{ 0x04ce, 0x30db }, + .{ 0x04cf, 0x30de }, + .{ 0x04d0, 0x30df }, + .{ 0x04d1, 0x30e0 }, + .{ 0x04d2, 0x30e1 }, + .{ 0x04d3, 0x30e2 }, + .{ 0x04d4, 0x30e4 }, + .{ 0x04d5, 0x30e6 }, + .{ 0x04d6, 0x30e8 }, + .{ 0x04d7, 0x30e9 }, + .{ 0x04d8, 0x30ea }, + .{ 0x04d9, 0x30eb }, + .{ 0x04da, 0x30ec }, + .{ 0x04db, 0x30ed }, + .{ 0x04dc, 0x30ef }, + .{ 0x04dd, 0x30f3 }, + .{ 0x04de, 0x309b }, + .{ 0x04df, 0x309c }, + .{ 0x05ac, 0x060c }, + .{ 0x05bb, 0x061b }, + .{ 0x05bf, 0x061f }, + .{ 0x05c1, 0x0621 }, + .{ 0x05c2, 0x0622 }, + .{ 0x05c3, 0x0623 }, + .{ 0x05c4, 0x0624 }, + .{ 0x05c5, 0x0625 }, + .{ 0x05c6, 0x0626 }, + .{ 0x05c7, 0x0627 }, + .{ 0x05c8, 0x0628 }, + .{ 0x05c9, 0x0629 }, + .{ 0x05ca, 0x062a }, + .{ 0x05cb, 0x062b }, + .{ 0x05cc, 0x062c }, + .{ 0x05cd, 0x062d }, + .{ 0x05ce, 0x062e }, + .{ 0x05cf, 0x062f }, + .{ 0x05d0, 0x0630 }, + .{ 0x05d1, 0x0631 }, + .{ 0x05d2, 0x0632 }, + .{ 0x05d3, 0x0633 }, + .{ 0x05d4, 0x0634 }, + .{ 0x05d5, 0x0635 }, + .{ 0x05d6, 0x0636 }, + .{ 0x05d7, 0x0637 }, + .{ 0x05d8, 0x0638 }, + .{ 0x05d9, 0x0639 }, + .{ 0x05da, 0x063a }, + .{ 0x05e0, 0x0640 }, + .{ 0x05e1, 0x0641 }, + .{ 0x05e2, 0x0642 }, + .{ 0x05e3, 0x0643 }, + .{ 0x05e4, 0x0644 }, + .{ 0x05e5, 0x0645 }, + .{ 0x05e6, 0x0646 }, + .{ 0x05e7, 0x0647 }, + .{ 0x05e8, 0x0648 }, + .{ 0x05e9, 0x0649 }, + .{ 0x05ea, 0x064a }, + .{ 0x05eb, 0x064b }, + .{ 0x05ec, 0x064c }, + .{ 0x05ed, 0x064d }, + .{ 0x05ee, 0x064e }, + .{ 0x05ef, 0x064f }, + .{ 0x05f0, 0x0650 }, + .{ 0x05f1, 0x0651 }, + .{ 0x05f2, 0x0652 }, + .{ 0x06a1, 0x0452 }, + .{ 0x06a2, 0x0453 }, + .{ 0x06a3, 0x0451 }, + .{ 0x06a4, 0x0454 }, + .{ 0x06a5, 0x0455 }, + .{ 0x06a6, 0x0456 }, + .{ 0x06a7, 0x0457 }, + .{ 0x06a8, 0x0458 }, + .{ 0x06a9, 0x0459 }, + .{ 0x06aa, 0x045a }, + .{ 0x06ab, 0x045b }, + .{ 0x06ac, 0x045c }, + .{ 0x06ae, 0x045e }, + .{ 0x06af, 0x045f }, + .{ 0x06b0, 0x2116 }, + .{ 0x06b1, 0x0402 }, + .{ 0x06b2, 0x0403 }, + .{ 0x06b3, 0x0401 }, + .{ 0x06b4, 0x0404 }, + .{ 0x06b5, 0x0405 }, + .{ 0x06b6, 0x0406 }, + .{ 0x06b7, 0x0407 }, + .{ 0x06b8, 0x0408 }, + .{ 0x06b9, 0x0409 }, + .{ 0x06ba, 0x040a }, + .{ 0x06bb, 0x040b }, + .{ 0x06bc, 0x040c }, + .{ 0x06be, 0x040e }, + .{ 0x06bf, 0x040f }, + .{ 0x06c0, 0x044e }, + .{ 0x06c1, 0x0430 }, + .{ 0x06c2, 0x0431 }, + .{ 0x06c3, 0x0446 }, + .{ 0x06c4, 0x0434 }, + .{ 0x06c5, 0x0435 }, + .{ 0x06c6, 0x0444 }, + .{ 0x06c7, 0x0433 }, + .{ 0x06c8, 0x0445 }, + .{ 0x06c9, 0x0438 }, + .{ 0x06ca, 0x0439 }, + .{ 0x06cb, 0x043a }, + .{ 0x06cc, 0x043b }, + .{ 0x06cd, 0x043c }, + .{ 0x06ce, 0x043d }, + .{ 0x06cf, 0x043e }, + .{ 0x06d0, 0x043f }, + .{ 0x06d1, 0x044f }, + .{ 0x06d2, 0x0440 }, + .{ 0x06d3, 0x0441 }, + .{ 0x06d4, 0x0442 }, + .{ 0x06d5, 0x0443 }, + .{ 0x06d6, 0x0436 }, + .{ 0x06d7, 0x0432 }, + .{ 0x06d8, 0x044c }, + .{ 0x06d9, 0x044b }, + .{ 0x06da, 0x0437 }, + .{ 0x06db, 0x0448 }, + .{ 0x06dc, 0x044d }, + .{ 0x06dd, 0x0449 }, + .{ 0x06de, 0x0447 }, + .{ 0x06df, 0x044a }, + .{ 0x06e0, 0x042e }, + .{ 0x06e1, 0x0410 }, + .{ 0x06e2, 0x0411 }, + .{ 0x06e3, 0x0426 }, + .{ 0x06e4, 0x0414 }, + .{ 0x06e5, 0x0415 }, + .{ 0x06e6, 0x0424 }, + .{ 0x06e7, 0x0413 }, + .{ 0x06e8, 0x0425 }, + .{ 0x06e9, 0x0418 }, + .{ 0x06ea, 0x0419 }, + .{ 0x06eb, 0x041a }, + .{ 0x06ec, 0x041b }, + .{ 0x06ed, 0x041c }, + .{ 0x06ee, 0x041d }, + .{ 0x06ef, 0x041e }, + .{ 0x06f0, 0x041f }, + .{ 0x06f1, 0x042f }, + .{ 0x06f2, 0x0420 }, + .{ 0x06f3, 0x0421 }, + .{ 0x06f4, 0x0422 }, + .{ 0x06f5, 0x0423 }, + .{ 0x06f6, 0x0416 }, + .{ 0x06f7, 0x0412 }, + .{ 0x06f8, 0x042c }, + .{ 0x06f9, 0x042b }, + .{ 0x06fa, 0x0417 }, + .{ 0x06fb, 0x0428 }, + .{ 0x06fc, 0x042d }, + .{ 0x06fd, 0x0429 }, + .{ 0x06fe, 0x0427 }, + .{ 0x06ff, 0x042a }, + .{ 0x07a1, 0x0386 }, + .{ 0x07a2, 0x0388 }, + .{ 0x07a3, 0x0389 }, + .{ 0x07a4, 0x038a }, + .{ 0x07a5, 0x03aa }, + .{ 0x07a7, 0x038c }, + .{ 0x07a8, 0x038e }, + .{ 0x07a9, 0x03ab }, + .{ 0x07ab, 0x038f }, + .{ 0x07ae, 0x0385 }, + .{ 0x07af, 0x2015 }, + .{ 0x07b1, 0x03ac }, + .{ 0x07b2, 0x03ad }, + .{ 0x07b3, 0x03ae }, + .{ 0x07b4, 0x03af }, + .{ 0x07b5, 0x03ca }, + .{ 0x07b6, 0x0390 }, + .{ 0x07b7, 0x03cc }, + .{ 0x07b8, 0x03cd }, + .{ 0x07b9, 0x03cb }, + .{ 0x07ba, 0x03b0 }, + .{ 0x07bb, 0x03ce }, + .{ 0x07c1, 0x0391 }, + .{ 0x07c2, 0x0392 }, + .{ 0x07c3, 0x0393 }, + .{ 0x07c4, 0x0394 }, + .{ 0x07c5, 0x0395 }, + .{ 0x07c6, 0x0396 }, + .{ 0x07c7, 0x0397 }, + .{ 0x07c8, 0x0398 }, + .{ 0x07c9, 0x0399 }, + .{ 0x07ca, 0x039a }, + .{ 0x07cb, 0x039b }, + .{ 0x07cc, 0x039c }, + .{ 0x07cd, 0x039d }, + .{ 0x07ce, 0x039e }, + .{ 0x07cf, 0x039f }, + .{ 0x07d0, 0x03a0 }, + .{ 0x07d1, 0x03a1 }, + .{ 0x07d2, 0x03a3 }, + .{ 0x07d4, 0x03a4 }, + .{ 0x07d5, 0x03a5 }, + .{ 0x07d6, 0x03a6 }, + .{ 0x07d7, 0x03a7 }, + .{ 0x07d8, 0x03a8 }, + .{ 0x07d9, 0x03a9 }, + .{ 0x07e1, 0x03b1 }, + .{ 0x07e2, 0x03b2 }, + .{ 0x07e3, 0x03b3 }, + .{ 0x07e4, 0x03b4 }, + .{ 0x07e5, 0x03b5 }, + .{ 0x07e6, 0x03b6 }, + .{ 0x07e7, 0x03b7 }, + .{ 0x07e8, 0x03b8 }, + .{ 0x07e9, 0x03b9 }, + .{ 0x07ea, 0x03ba }, + .{ 0x07eb, 0x03bb }, + .{ 0x07ec, 0x03bc }, + .{ 0x07ed, 0x03bd }, + .{ 0x07ee, 0x03be }, + .{ 0x07ef, 0x03bf }, + .{ 0x07f0, 0x03c0 }, + .{ 0x07f1, 0x03c1 }, + .{ 0x07f2, 0x03c3 }, + .{ 0x07f3, 0x03c2 }, + .{ 0x07f4, 0x03c4 }, + .{ 0x07f5, 0x03c5 }, + .{ 0x07f6, 0x03c6 }, + .{ 0x07f7, 0x03c7 }, + .{ 0x07f8, 0x03c8 }, + .{ 0x07f9, 0x03c9 }, + .{ 0x08a1, 0x23b7 }, + .{ 0x08a2, 0x250c }, + .{ 0x08a3, 0x2500 }, + .{ 0x08a4, 0x2320 }, + .{ 0x08a5, 0x2321 }, + .{ 0x08a6, 0x2502 }, + .{ 0x08a7, 0x23a1 }, + .{ 0x08a8, 0x23a3 }, + .{ 0x08a9, 0x23a4 }, + .{ 0x08aa, 0x23a6 }, + .{ 0x08ab, 0x239b }, + .{ 0x08ac, 0x239d }, + .{ 0x08ad, 0x239e }, + .{ 0x08ae, 0x23a0 }, + .{ 0x08af, 0x23a8 }, + .{ 0x08b0, 0x23ac }, + .{ 0x08bc, 0x2264 }, + .{ 0x08bd, 0x2260 }, + .{ 0x08be, 0x2265 }, + .{ 0x08bf, 0x222b }, + .{ 0x08c0, 0x2234 }, + .{ 0x08c1, 0x221d }, + .{ 0x08c2, 0x221e }, + .{ 0x08c5, 0x2207 }, + .{ 0x08c8, 0x223c }, + .{ 0x08c9, 0x2243 }, + .{ 0x08cd, 0x21d4 }, + .{ 0x08ce, 0x21d2 }, + .{ 0x08cf, 0x2261 }, + .{ 0x08d6, 0x221a }, + .{ 0x08da, 0x2282 }, + .{ 0x08db, 0x2283 }, + .{ 0x08dc, 0x2229 }, + .{ 0x08dd, 0x222a }, + .{ 0x08de, 0x2227 }, + .{ 0x08df, 0x2228 }, + .{ 0x08ef, 0x2202 }, + .{ 0x08f6, 0x0192 }, + .{ 0x08fb, 0x2190 }, + .{ 0x08fc, 0x2191 }, + .{ 0x08fd, 0x2192 }, + .{ 0x08fe, 0x2193 }, + .{ 0x09e0, 0x25c6 }, + .{ 0x09e1, 0x2592 }, + .{ 0x09e2, 0x2409 }, + .{ 0x09e3, 0x240c }, + .{ 0x09e4, 0x240d }, + .{ 0x09e5, 0x240a }, + .{ 0x09e8, 0x2424 }, + .{ 0x09e9, 0x240b }, + .{ 0x09ea, 0x2518 }, + .{ 0x09eb, 0x2510 }, + .{ 0x09ec, 0x250c }, + .{ 0x09ed, 0x2514 }, + .{ 0x09ee, 0x253c }, + .{ 0x09ef, 0x23ba }, + .{ 0x09f0, 0x23bb }, + .{ 0x09f1, 0x2500 }, + .{ 0x09f2, 0x23bc }, + .{ 0x09f3, 0x23bd }, + .{ 0x09f4, 0x251c }, + .{ 0x09f5, 0x2524 }, + .{ 0x09f6, 0x2534 }, + .{ 0x09f7, 0x252c }, + .{ 0x09f8, 0x2502 }, + .{ 0x0aa1, 0x2003 }, + .{ 0x0aa2, 0x2002 }, + .{ 0x0aa3, 0x2004 }, + .{ 0x0aa4, 0x2005 }, + .{ 0x0aa5, 0x2007 }, + .{ 0x0aa6, 0x2008 }, + .{ 0x0aa7, 0x2009 }, + .{ 0x0aa8, 0x200a }, + .{ 0x0aa9, 0x2014 }, + .{ 0x0aaa, 0x2013 }, + .{ 0x0aae, 0x2026 }, + .{ 0x0aaf, 0x2025 }, + .{ 0x0ab0, 0x2153 }, + .{ 0x0ab1, 0x2154 }, + .{ 0x0ab2, 0x2155 }, + .{ 0x0ab3, 0x2156 }, + .{ 0x0ab4, 0x2157 }, + .{ 0x0ab5, 0x2158 }, + .{ 0x0ab6, 0x2159 }, + .{ 0x0ab7, 0x215a }, + .{ 0x0ab8, 0x2105 }, + .{ 0x0abb, 0x2012 }, + .{ 0x0abc, 0x2329 }, + .{ 0x0abe, 0x232a }, + .{ 0x0ac3, 0x215b }, + .{ 0x0ac4, 0x215c }, + .{ 0x0ac5, 0x215d }, + .{ 0x0ac6, 0x215e }, + .{ 0x0ac9, 0x2122 }, + .{ 0x0aca, 0x2613 }, + .{ 0x0acc, 0x25c1 }, + .{ 0x0acd, 0x25b7 }, + .{ 0x0ace, 0x25cb }, + .{ 0x0acf, 0x25af }, + .{ 0x0ad0, 0x2018 }, + .{ 0x0ad1, 0x2019 }, + .{ 0x0ad2, 0x201c }, + .{ 0x0ad3, 0x201d }, + .{ 0x0ad4, 0x211e }, + .{ 0x0ad6, 0x2032 }, + .{ 0x0ad7, 0x2033 }, + .{ 0x0ad9, 0x271d }, + .{ 0x0adb, 0x25ac }, + .{ 0x0adc, 0x25c0 }, + .{ 0x0add, 0x25b6 }, + .{ 0x0ade, 0x25cf }, + .{ 0x0adf, 0x25ae }, + .{ 0x0ae0, 0x25e6 }, + .{ 0x0ae1, 0x25ab }, + .{ 0x0ae2, 0x25ad }, + .{ 0x0ae3, 0x25b3 }, + .{ 0x0ae4, 0x25bd }, + .{ 0x0ae5, 0x2606 }, + .{ 0x0ae6, 0x2022 }, + .{ 0x0ae7, 0x25aa }, + .{ 0x0ae8, 0x25b2 }, + .{ 0x0ae9, 0x25bc }, + .{ 0x0aea, 0x261c }, + .{ 0x0aeb, 0x261e }, + .{ 0x0aec, 0x2663 }, + .{ 0x0aed, 0x2666 }, + .{ 0x0aee, 0x2665 }, + .{ 0x0af0, 0x2720 }, + .{ 0x0af1, 0x2020 }, + .{ 0x0af2, 0x2021 }, + .{ 0x0af3, 0x2713 }, + .{ 0x0af4, 0x2717 }, + .{ 0x0af5, 0x266f }, + .{ 0x0af6, 0x266d }, + .{ 0x0af7, 0x2642 }, + .{ 0x0af8, 0x2640 }, + .{ 0x0af9, 0x260e }, + .{ 0x0afa, 0x2315 }, + .{ 0x0afb, 0x2117 }, + .{ 0x0afc, 0x2038 }, + .{ 0x0afd, 0x201a }, + .{ 0x0afe, 0x201e }, + .{ 0x0ba3, 0x003c }, + .{ 0x0ba6, 0x003e }, + .{ 0x0ba8, 0x2228 }, + .{ 0x0ba9, 0x2227 }, + .{ 0x0bc0, 0x00af }, + .{ 0x0bc2, 0x22a5 }, + .{ 0x0bc3, 0x2229 }, + .{ 0x0bc4, 0x230a }, + .{ 0x0bc6, 0x005f }, + .{ 0x0bca, 0x2218 }, + .{ 0x0bcc, 0x2395 }, + .{ 0x0bce, 0x22a4 }, + .{ 0x0bcf, 0x25cb }, + .{ 0x0bd3, 0x2308 }, + .{ 0x0bd6, 0x222a }, + .{ 0x0bd8, 0x2283 }, + .{ 0x0bda, 0x2282 }, + .{ 0x0bdc, 0x22a2 }, + .{ 0x0bfc, 0x22a3 }, + .{ 0x0cdf, 0x2017 }, + .{ 0x0ce0, 0x05d0 }, + .{ 0x0ce1, 0x05d1 }, + .{ 0x0ce2, 0x05d2 }, + .{ 0x0ce3, 0x05d3 }, + .{ 0x0ce4, 0x05d4 }, + .{ 0x0ce5, 0x05d5 }, + .{ 0x0ce6, 0x05d6 }, + .{ 0x0ce7, 0x05d7 }, + .{ 0x0ce8, 0x05d8 }, + .{ 0x0ce9, 0x05d9 }, + .{ 0x0cea, 0x05da }, + .{ 0x0ceb, 0x05db }, + .{ 0x0cec, 0x05dc }, + .{ 0x0ced, 0x05dd }, + .{ 0x0cee, 0x05de }, + .{ 0x0cef, 0x05df }, + .{ 0x0cf0, 0x05e0 }, + .{ 0x0cf1, 0x05e1 }, + .{ 0x0cf2, 0x05e2 }, + .{ 0x0cf3, 0x05e3 }, + .{ 0x0cf4, 0x05e4 }, + .{ 0x0cf5, 0x05e5 }, + .{ 0x0cf6, 0x05e6 }, + .{ 0x0cf7, 0x05e7 }, + .{ 0x0cf8, 0x05e8 }, + .{ 0x0cf9, 0x05e9 }, + .{ 0x0cfa, 0x05ea }, + .{ 0x0da1, 0x0e01 }, + .{ 0x0da2, 0x0e02 }, + .{ 0x0da3, 0x0e03 }, + .{ 0x0da4, 0x0e04 }, + .{ 0x0da5, 0x0e05 }, + .{ 0x0da6, 0x0e06 }, + .{ 0x0da7, 0x0e07 }, + .{ 0x0da8, 0x0e08 }, + .{ 0x0da9, 0x0e09 }, + .{ 0x0daa, 0x0e0a }, + .{ 0x0dab, 0x0e0b }, + .{ 0x0dac, 0x0e0c }, + .{ 0x0dad, 0x0e0d }, + .{ 0x0dae, 0x0e0e }, + .{ 0x0daf, 0x0e0f }, + .{ 0x0db0, 0x0e10 }, + .{ 0x0db1, 0x0e11 }, + .{ 0x0db2, 0x0e12 }, + .{ 0x0db3, 0x0e13 }, + .{ 0x0db4, 0x0e14 }, + .{ 0x0db5, 0x0e15 }, + .{ 0x0db6, 0x0e16 }, + .{ 0x0db7, 0x0e17 }, + .{ 0x0db8, 0x0e18 }, + .{ 0x0db9, 0x0e19 }, + .{ 0x0dba, 0x0e1a }, + .{ 0x0dbb, 0x0e1b }, + .{ 0x0dbc, 0x0e1c }, + .{ 0x0dbd, 0x0e1d }, + .{ 0x0dbe, 0x0e1e }, + .{ 0x0dbf, 0x0e1f }, + .{ 0x0dc0, 0x0e20 }, + .{ 0x0dc1, 0x0e21 }, + .{ 0x0dc2, 0x0e22 }, + .{ 0x0dc3, 0x0e23 }, + .{ 0x0dc4, 0x0e24 }, + .{ 0x0dc5, 0x0e25 }, + .{ 0x0dc6, 0x0e26 }, + .{ 0x0dc7, 0x0e27 }, + .{ 0x0dc8, 0x0e28 }, + .{ 0x0dc9, 0x0e29 }, + .{ 0x0dca, 0x0e2a }, + .{ 0x0dcb, 0x0e2b }, + .{ 0x0dcc, 0x0e2c }, + .{ 0x0dcd, 0x0e2d }, + .{ 0x0dce, 0x0e2e }, + .{ 0x0dcf, 0x0e2f }, + .{ 0x0dd0, 0x0e30 }, + .{ 0x0dd1, 0x0e31 }, + .{ 0x0dd2, 0x0e32 }, + .{ 0x0dd3, 0x0e33 }, + .{ 0x0dd4, 0x0e34 }, + .{ 0x0dd5, 0x0e35 }, + .{ 0x0dd6, 0x0e36 }, + .{ 0x0dd7, 0x0e37 }, + .{ 0x0dd8, 0x0e38 }, + .{ 0x0dd9, 0x0e39 }, + .{ 0x0dda, 0x0e3a }, + .{ 0x0ddf, 0x0e3f }, + .{ 0x0de0, 0x0e40 }, + .{ 0x0de1, 0x0e41 }, + .{ 0x0de2, 0x0e42 }, + .{ 0x0de3, 0x0e43 }, + .{ 0x0de4, 0x0e44 }, + .{ 0x0de5, 0x0e45 }, + .{ 0x0de6, 0x0e46 }, + .{ 0x0de7, 0x0e47 }, + .{ 0x0de8, 0x0e48 }, + .{ 0x0de9, 0x0e49 }, + .{ 0x0dea, 0x0e4a }, + .{ 0x0deb, 0x0e4b }, + .{ 0x0dec, 0x0e4c }, + .{ 0x0ded, 0x0e4d }, + .{ 0x0df0, 0x0e50 }, + .{ 0x0df1, 0x0e51 }, + .{ 0x0df2, 0x0e52 }, + .{ 0x0df3, 0x0e53 }, + .{ 0x0df4, 0x0e54 }, + .{ 0x0df5, 0x0e55 }, + .{ 0x0df6, 0x0e56 }, + .{ 0x0df7, 0x0e57 }, + .{ 0x0df8, 0x0e58 }, + .{ 0x0df9, 0x0e59 }, + .{ 0x0ea1, 0x3131 }, + .{ 0x0ea2, 0x3132 }, + .{ 0x0ea3, 0x3133 }, + .{ 0x0ea4, 0x3134 }, + .{ 0x0ea5, 0x3135 }, + .{ 0x0ea6, 0x3136 }, + .{ 0x0ea7, 0x3137 }, + .{ 0x0ea8, 0x3138 }, + .{ 0x0ea9, 0x3139 }, + .{ 0x0eaa, 0x313a }, + .{ 0x0eab, 0x313b }, + .{ 0x0eac, 0x313c }, + .{ 0x0ead, 0x313d }, + .{ 0x0eae, 0x313e }, + .{ 0x0eaf, 0x313f }, + .{ 0x0eb0, 0x3140 }, + .{ 0x0eb1, 0x3141 }, + .{ 0x0eb2, 0x3142 }, + .{ 0x0eb3, 0x3143 }, + .{ 0x0eb4, 0x3144 }, + .{ 0x0eb5, 0x3145 }, + .{ 0x0eb6, 0x3146 }, + .{ 0x0eb7, 0x3147 }, + .{ 0x0eb8, 0x3148 }, + .{ 0x0eb9, 0x3149 }, + .{ 0x0eba, 0x314a }, + .{ 0x0ebb, 0x314b }, + .{ 0x0ebc, 0x314c }, + .{ 0x0ebd, 0x314d }, + .{ 0x0ebe, 0x314e }, + .{ 0x0ebf, 0x314f }, + .{ 0x0ec0, 0x3150 }, + .{ 0x0ec1, 0x3151 }, + .{ 0x0ec2, 0x3152 }, + .{ 0x0ec3, 0x3153 }, + .{ 0x0ec4, 0x3154 }, + .{ 0x0ec5, 0x3155 }, + .{ 0x0ec6, 0x3156 }, + .{ 0x0ec7, 0x3157 }, + .{ 0x0ec8, 0x3158 }, + .{ 0x0ec9, 0x3159 }, + .{ 0x0eca, 0x315a }, + .{ 0x0ecb, 0x315b }, + .{ 0x0ecc, 0x315c }, + .{ 0x0ecd, 0x315d }, + .{ 0x0ece, 0x315e }, + .{ 0x0ecf, 0x315f }, + .{ 0x0ed0, 0x3160 }, + .{ 0x0ed1, 0x3161 }, + .{ 0x0ed2, 0x3162 }, + .{ 0x0ed3, 0x3163 }, + .{ 0x0ed4, 0x11a8 }, + .{ 0x0ed5, 0x11a9 }, + .{ 0x0ed6, 0x11aa }, + .{ 0x0ed7, 0x11ab }, + .{ 0x0ed8, 0x11ac }, + .{ 0x0ed9, 0x11ad }, + .{ 0x0eda, 0x11ae }, + .{ 0x0edb, 0x11af }, + .{ 0x0edc, 0x11b0 }, + .{ 0x0edd, 0x11b1 }, + .{ 0x0ede, 0x11b2 }, + .{ 0x0edf, 0x11b3 }, + .{ 0x0ee0, 0x11b4 }, + .{ 0x0ee1, 0x11b5 }, + .{ 0x0ee2, 0x11b6 }, + .{ 0x0ee3, 0x11b7 }, + .{ 0x0ee4, 0x11b8 }, + .{ 0x0ee5, 0x11b9 }, + .{ 0x0ee6, 0x11ba }, + .{ 0x0ee7, 0x11bb }, + .{ 0x0ee8, 0x11bc }, + .{ 0x0ee9, 0x11bd }, + .{ 0x0eea, 0x11be }, + .{ 0x0eeb, 0x11bf }, + .{ 0x0eec, 0x11c0 }, + .{ 0x0eed, 0x11c1 }, + .{ 0x0eee, 0x11c2 }, + .{ 0x0eef, 0x316d }, + .{ 0x0ef0, 0x3171 }, + .{ 0x0ef1, 0x3178 }, + .{ 0x0ef2, 0x317f }, + .{ 0x0ef3, 0x3181 }, + .{ 0x0ef4, 0x3184 }, + .{ 0x0ef5, 0x3186 }, + .{ 0x0ef6, 0x318d }, + .{ 0x0ef7, 0x318e }, + .{ 0x0ef8, 0x11eb }, + .{ 0x0ef9, 0x11f0 }, + .{ 0x0efa, 0x11f9 }, + .{ 0x0eff, 0x20a9 }, + .{ 0x13a4, 0x20ac }, + .{ 0x13bc, 0x0152 }, + .{ 0x13bd, 0x0153 }, + .{ 0x13be, 0x0178 }, + .{ 0x20ac, 0x20ac }, + .{ 0xfe50, '`' }, + .{ 0xfe51, 0x00b4 }, + .{ 0xfe52, '^' }, + .{ 0xfe53, '~' }, + .{ 0xfe54, 0x00af }, + .{ 0xfe55, 0x02d8 }, + .{ 0xfe56, 0x02d9 }, + .{ 0xfe57, 0x00a8 }, + .{ 0xfe58, 0x02da }, + .{ 0xfe59, 0x02dd }, + .{ 0xfe5a, 0x02c7 }, + .{ 0xfe5b, 0x00b8 }, + .{ 0xfe5c, 0x02db }, + .{ 0xfe5d, 0x037a }, + .{ 0xfe5e, 0x309b }, + .{ 0xfe5f, 0x309c }, + .{ 0xfe63, '/' }, + .{ 0xfe64, 0x02bc }, + .{ 0xfe65, 0x02bd }, + .{ 0xfe66, 0x02f5 }, + .{ 0xfe67, 0x02f3 }, + .{ 0xfe68, 0x02cd }, + .{ 0xfe69, 0xa788 }, + .{ 0xfe6a, 0x02f7 }, + .{ 0xfe6e, ',' }, + .{ 0xfe6f, 0x00a4 }, + .{ 0xfe80, 'a' }, // XK_dead_a + .{ 0xfe81, 'A' }, // XK_dead_A + .{ 0xfe82, 'e' }, // XK_dead_e + .{ 0xfe83, 'E' }, // XK_dead_E + .{ 0xfe84, 'i' }, // XK_dead_i + .{ 0xfe85, 'I' }, // XK_dead_I + .{ 0xfe86, 'o' }, // XK_dead_o + .{ 0xfe87, 'O' }, // XK_dead_O + .{ 0xfe88, 'u' }, // XK_dead_u + .{ 0xfe89, 'U' }, // XK_dead_U + .{ 0xfe8a, 0x0259 }, + .{ 0xfe8b, 0x018f }, + .{ 0xfe8c, 0x00b5 }, + .{ 0xfe90, '_' }, + .{ 0xfe91, 0x02c8 }, + .{ 0xfe92, 0x02cc }, + .{ 0xff80, ' ' }, // XKB_KEY_KP_Space + .{ 0xff95, 0x0037 }, // XKB_KEY_KP_7 + .{ 0xff96, 0x0034 }, // XKB_KEY_KP_4 + .{ 0xff97, 0x0038 }, // XKB_KEY_KP_8 + .{ 0xff98, 0x0036 }, // XKB_KEY_KP_6 + .{ 0xff99, 0x0032 }, // XKB_KEY_KP_2 + .{ 0xff9a, 0x0039 }, // XKB_KEY_KP_9 + .{ 0xff9b, 0x0033 }, // XKB_KEY_KP_3 + .{ 0xff9c, 0x0031 }, // XKB_KEY_KP_1 + .{ 0xff9d, 0x0035 }, // XKB_KEY_KP_5 + .{ 0xff9e, 0x0030 }, // XKB_KEY_KP_0 + .{ 0xffaa, '*' }, // XKB_KEY_KP_Multiply + .{ 0xffab, '+' }, // XKB_KEY_KP_Add + .{ 0xffac, ',' }, // XKB_KEY_KP_Separator + .{ 0xffad, '-' }, // XKB_KEY_KP_Subtract + .{ 0xffae, '.' }, // XKB_KEY_KP_Decimal + .{ 0xffaf, '/' }, // XKB_KEY_KP_Divide + .{ 0xffb0, 0x0030 }, // XKB_KEY_KP_0 + .{ 0xffb1, 0x0031 }, // XKB_KEY_KP_1 + .{ 0xffb2, 0x0032 }, // XKB_KEY_KP_2 + .{ 0xffb3, 0x0033 }, // XKB_KEY_KP_3 + .{ 0xffb4, 0x0034 }, // XKB_KEY_KP_4 + .{ 0xffb5, 0x0035 }, // XKB_KEY_KP_5 + .{ 0xffb6, 0x0036 }, // XKB_KEY_KP_6 + .{ 0xffb7, 0x0037 }, // XKB_KEY_KP_7 + .{ 0xffb8, 0x0038 }, // XKB_KEY_KP_8 + .{ 0xffb9, 0x0039 }, // XKB_KEY_KP_9 + .{ 0xffbd, '=' }, // XKB_KEY_KP_Equal +}; + +pub fn unicodeFromKeySym(keysym: c.KeySym) ?u21 { + var min: usize = 0; + var mid: usize = 0; + var max = keysym_table.len - 1; + + // First check for Latin-1 characters (1:1 mapping) + if ((keysym >= 0x0020 and keysym <= 0x007e) or + (keysym >= 0x00a0 and keysym <= 0x00ff)) + { + return @intCast(keysym); + } + + // Also check for directly encoded 24-bit UCS characters + if ((keysym & 0xff000000) == 0x01000000) { + return @intCast(keysym & 0x00ffffff); + } + + // Binary search in table + while (max >= min) { + mid = (min + max) / 2; + if (keysym_table[mid][0] < keysym) { + min = mid + 1; + } else if (keysym_table[mid][0] > keysym) { + max = mid - 1; + } else { + return keysym_table[mid][1]; + } + } + + return null; +}