From 8c447eac1a63c6e7c66e74d7a72c29eaf82aeea6 Mon Sep 17 00:00:00 2001 From: kayomn Date: Fri, 8 Aug 2025 13:52:45 +0100 Subject: [PATCH] Add graphics command queues --- src/coral/coral.zig | 31 +++-- src/coral/meta.zig | 35 +++++ src/demos/graphics.zig | 5 + src/ona/App.zig | 12 +- src/ona/App/Behavior.zig | 76 +++------- src/ona/App/Setup.zig | 14 +- src/ona/gfx.zig | 279 ++++--------------------------------- src/ona/gfx/Context.zig | 250 +++++++++++++++++++++++++++++++++ src/ona/gfx/composite.frag | 3 + src/ona/gfx/composite.vert | 25 ++++ src/ona/gfx/glsl.zig | 136 ++++++++++++++++++ src/ona/hid.zig | 34 +++-- src/ona/ona.zig | 1 - 13 files changed, 554 insertions(+), 347 deletions(-) create mode 100644 src/coral/meta.zig create mode 100644 src/ona/gfx/Context.zig create mode 100644 src/ona/gfx/composite.frag create mode 100644 src/ona/gfx/composite.vert diff --git a/src/coral/coral.zig b/src/coral/coral.zig index b2b9ee2..120e950 100644 --- a/src/coral/coral.zig +++ b/src/coral/coral.zig @@ -12,6 +12,8 @@ pub const heap = @import("./heap.zig"); pub const map = @import("./map.zig"); +pub const meta = @import("./meta.zig"); + pub const scalars = @import("./scalars.zig"); pub const stack = @import("./stack.zig"); @@ -113,15 +115,26 @@ pub fn KeyValuePair(comptime Key: type, comptime Value: type) type { }; } -pub fn require(function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) switch (@typeInfo(@typeInfo(@TypeOf(function)).@"fn".return_type.?)) { - .error_union => |error_union| error_union.payload, - else => @compileError("fn parameter `function` must return an error union"), -} { - return @call(.auto, function, args) catch |call_error| { - if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); - } +pub fn expect(function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) meta.Unwrapped(@typeInfo(@TypeOf(function)).@"fn".return_type.?) { + const Returns = @typeInfo(@TypeOf(function)).@"fn".return_type.?; - std.debug.panic("requirement failed: {s}", .{@errorName(call_error)}); + return switch (@typeInfo(Returns)) { + .error_union => @call(.auto, function, args) catch |call_error| { + if (@errorReturnTrace()) |trace| { + std.debug.dumpStackTrace(trace.*); + } + + std.debug.panic("requirement failed: {s}", .{@errorName(call_error)}); + }, + + .optional => @call(.auto, function, args) orelse { + if (@errorReturnTrace()) |trace| { + std.debug.dumpStackTrace(trace.*); + } + + @panic("requirement failed: null"); + }, + + else => @call(.auto, function, args), }; } diff --git a/src/coral/meta.zig b/src/coral/meta.zig new file mode 100644 index 0000000..e64fced --- /dev/null +++ b/src/coral/meta.zig @@ -0,0 +1,35 @@ +const std = @import("std"); + +pub fn Unwrapped(comptime Value: type) type { + return switch (@typeInfo(Value)) { + .error_union => |error_union| error_union.payload, + .optional => |optional| optional.child, + else => Value, + }; +} + +pub fn isContainer(@"type": std.builtin.Type) bool { + return switch (@"type") { + .@"struct", .@"union", .@"enum", .@"opaque" => true, + else => false, + }; +} + +pub fn hasFn(comptime Type: type, comptime fn_name: []const u8) ?std.builtin.Type.Fn { + const @"type" = @typeInfo(Type); + + if (!isContainer(@"type")) { + return null; + } + + if (!@hasDecl(Type, fn_name)) { + return null; + } + + const Fn = @TypeOf(@field(Type, fn_name)); + + return switch (@typeInfo(Fn)) { + .@"fn" => |@"fn"| @"fn", + else => null, + }; +} diff --git a/src/demos/graphics.zig b/src/demos/graphics.zig index 67f5f4c..0e22cd5 100644 --- a/src/demos/graphics.zig +++ b/src/demos/graphics.zig @@ -6,5 +6,10 @@ pub fn main() void { ona.realtime_app .with(.initModule(ona.hid)) .with(.initModule(ona.gfx)) + .with(.initSystem(.render, .of(render))) .run(); } + +fn render(commands: ona.gfx.Commands) void { + _ = commands; +} diff --git a/src/ona/App.zig b/src/ona/App.zig index 7af8b87..fb75e33 100644 --- a/src/ona/App.zig +++ b/src/ona/App.zig @@ -65,7 +65,7 @@ pub fn hasState(self: *Self, comptime State: type) ?*State { return null; } -pub fn init() std.mem.Allocator.Error!Self { +pub fn init() error{OutOfMemory}!Self { var self = Self{ .initialized_states = .empty, .named_systems = .empty, @@ -80,19 +80,13 @@ pub fn init() std.mem.Allocator.Error!Self { return self; } -pub fn on(self: *Self, comptime schedule: anytype, behavior: *const Behavior) std.mem.Allocator.Error!void { +pub fn on(self: *Self, comptime schedule: anytype, behavior: *const Behavior) error{OutOfMemory}!void { const schedule_name = scheduleName(schedule); const systems = self.named_systems.get(schedule_name) orelse (try self.named_systems.insert(coral.heap.allocator, schedule_name, .empty)).?; try systems.insert(self, behavior); } -pub fn putState(self: *Self, state: anytype) std.mem.Allocator.Error!*@TypeOf(state) { - try self.setState(state); - - return self.hasState(@TypeOf(state)).?; -} - pub fn run(self: *Self, tasks: *coral.asio.TaskQueue, comptime schedule: anytype) RunError!void { const schedule_name = scheduleName(schedule); const systems = self.named_systems.get(schedule_name) orelse (try self.named_systems.insert(coral.heap.allocator, schedule_name, .empty)).?; @@ -100,7 +94,7 @@ pub fn run(self: *Self, tasks: *coral.asio.TaskQueue, comptime schedule: anytype try systems.run(self, tasks); } -pub fn setState(self: *Self, state: anytype) std.mem.Allocator.Error!void { +pub fn setState(self: *Self, state: anytype) error{OutOfMemory}!void { var boxed_state = try coral.Box.init(coral.heap.allocator, state); errdefer { diff --git a/src/ona/App/Behavior.zig b/src/ona/App/Behavior.zig index 976712c..57e4181 100644 --- a/src/ona/App/Behavior.zig +++ b/src/ona/App/Behavior.zig @@ -65,13 +65,17 @@ const Param = struct { @compileError("generic behavior params are not permitted"); }; - const init_fn = hasFn(FnParam, "init", null) orelse { + const init_fn = coral.meta.hasFn(FnParam, "init") orelse { @compileError(std.fmt.comptimePrint("{s} must contain a .init declaration", .{@typeName(FnParam)})); }; comptime var uses_bind = false; - if (hasFn(FnParam, "bind", &.{})) |bind_fn| { + if (coral.meta.hasFn(FnParam, "bind")) |bind_fn| { + if (bind_fn.params.len != 0) { + @compileError(std.fmt.comptimePrint("{s}.bind cannot accept any parameters", .{@typeName(FnParam)})); + } + const State = switch (@typeInfo(bind_fn.return_type.?)) { .error_union => |error_union| error_union.payload, else => bind_fn.return_type.?, @@ -90,10 +94,13 @@ const Param = struct { uses_bind = true; } - if (init_fn.return_type != FnParam) { + if (coral.meta.Unwrapped(init_fn.return_type.?) != FnParam) { const param_type_name = @typeName(FnParam); - @compileError(std.fmt.comptimePrint("Fn {s}.init must return {s}", .{ param_type_name, param_type_name })); + @compileError(std.fmt.comptimePrint("Fn {s}.init must return some variation of {s}", .{ + param_type_name, + param_type_name, + })); } return .{ @@ -143,54 +150,6 @@ pub fn before(comptime self: *const Self, comptime dependant: *const Self) *cons return &afters.instance; } -fn hasFn(comptime SystemParam: type, comptime name: []const u8, comptime has_expected_param_types: ?[]const type) ?std.builtin.Type.Fn { - switch (@typeInfo(SystemParam)) { - .@"struct", .@"opaque" => {}, - - else => @compileError(std.fmt.comptimePrint("{s} must be a struct or opaque types", .{ - @typeName(SystemParam), - })), - } - - if (@hasDecl(SystemParam, name)) { - const named_fn = switch (@typeInfo(@TypeOf(@field(SystemParam, name)))) { - .@"fn" => |@"fn"| @"fn", - - else => @compileError(std.fmt.comptimePrint("{s}.{s} declaration must be made an fn type", .{ - @typeName(SystemParam), - name, - })), - }; - - if (has_expected_param_types) |expected_param_types| { - if (named_fn.params.len != expected_param_types.len) { - @compileError(std.fmt.comptimePrint("{s}.{s} fn must accept {d} parameters, not {d}", .{ - @typeName(SystemParam), - name, - expected_param_types.len, - named_fn.params.len, - })); - } - - inline for (named_fn.params, expected_param_types, 1..expected_param_types.len + 1) |fn_param, ExpectedParam, i| { - if (fn_param.type != ExpectedParam) { - @compileError(std.fmt.comptimePrint("Parameter {d} of {s}.{s} fn must be {s}, not {s}", .{ - i, - @typeName(SystemParam), - name, - @typeName(ExpectedParam), - @typeName(fn_param.type.?), - })); - } - } - } - - return named_fn; - } - - return null; -} - pub fn of(comptime call: anytype) *const Self { const Call = @TypeOf(call); @@ -255,17 +214,16 @@ pub fn of(comptime call: anytype) *const Self { } } - call_args[i] = @call(.auto, fn_param.type.?.init, init_args); + call_args[i] = coral.expect(fn_param.type.?.init, init_args); } - switch (@typeInfo(call_fn.return_type.?)) { - .void => @call(.auto, call, call_args), - .error_union => coral.require(call, call_args), - - else => @compileError(std.fmt.comptimePrint("parameter `call` must be fn (*ona.App) void or fn (*ona.App) !void, not {s}", .{ + if (coral.meta.Unwrapped(call_fn.return_type.?) != void) { + @compileError(std.fmt.comptimePrint("parameter `call` must return some variation of void", .{ @typeName(Call), - })), + })); } + + coral.expect(call, call_args); } const instance = Self{ diff --git a/src/ona/App/Setup.zig b/src/ona/App/Setup.zig index d4b0eb0..d84fd8c 100644 --- a/src/ona/App/Setup.zig +++ b/src/ona/App/Setup.zig @@ -29,7 +29,7 @@ pub fn init(comptime start: anytype) Self { switch (@typeInfo(start_fn.return_type.?)) { .error_union => |error_union| switch (error_union.payload) { - void => coral.require(start, .{app}), + void => coral.expect(start, .{app}), else => @compileError(error_message), }, @@ -52,12 +52,22 @@ pub fn initModule(comptime namespace: anytype) Self { return init(namespace.setup); } +pub fn initSystem(comptime schedule: anytype, comptime behavior: *const ona.App.Behavior) Self { + const system_initialization = struct { + fn apply(app: *ona.App) !void { + try app.on(schedule, behavior); + } + }; + + return init(system_initialization.apply); +} + pub fn run(self: Self) void { defer { coral.heap.traceLeaks(); } - var app = coral.require(ona.App.init, .{}); + var app = coral.expect(ona.App.init, .{}); defer { app.deinit(); diff --git a/src/ona/gfx.zig b/src/ona/gfx.zig index f917136..ea16a8a 100644 --- a/src/ona/gfx.zig +++ b/src/ona/gfx.zig @@ -1,8 +1,8 @@ -const builtin = @import("builtin"); +const Context = @import("./gfx/Context.zig"); const coral = @import("coral"); -const ext = @cImport({ +const c = @cImport({ @cInclude("SDL3/SDL.h"); @cInclude("shaderc/shaderc.h"); }); @@ -13,72 +13,14 @@ const ona = @import("./ona.zig"); const std = @import("std"); -const Context = struct { - window: *ext.SDL_Window, - frame_arena: std.heap.ArenaAllocator, - gpu_device: *ext.SDL_GPUDevice, - shader_compiler: ext.shaderc_compiler_t, - spirv_options: ext.shaderc_compile_options_t, +pub const Commands = struct { + pending: *Context.Queue, - pub const AssembleError = std.mem.Allocator.Error || error{BadSyntax}; - - pub const AssemblyKind = enum(c_int) { - vertex, - fragment, - }; - - pub fn assembleShader(self: *Context, kind: AssemblyKind, name: [*:0]const u8, source: []const u8, injections: anytype) AssembleError![]const u8 { - const frame_allocator = self.frame_arena.allocator(); - const injected_source = try glsl.inject(frame_allocator, source, 430, injections); - - const result = ext.shaderc_compile_into_spv(self.shader_compiler, injected_source.ptr, injected_source.len, switch (kind) { - .fragment => ext.shaderc_glsl_fragment_shader, - .vertex => ext.shaderc_glsl_vertex_shader, - }, name, "main", self.spirv_options) orelse { - return error.OutOfMemory; - }; - - defer { - ext.shaderc_result_release(result); - } - - return switch (ext.shaderc_result_get_compilation_status(result)) { - ext.shaderc_compilation_status_success => { - const compiled_len = ext.shaderc_result_get_length(result); - const compiled_ptr = ext.shaderc_result_get_bytes(result); - - return frame_allocator.dupe(u8, compiled_ptr[0..compiled_len]); - }, - - ext.shaderc_compilation_status_compilation_error, ext.shaderc_compilation_status_invalid_stage => { - std.log.err("{s}", .{ext.shaderc_result_get_error_message(result)}); - std.log.debug("problematic shader:\n{s}", .{injected_source}); - - return error.BadSyntax; - }, - - ext.shaderc_compilation_status_internal_error => @panic("shaderc internal compiler error"), - else => unreachable, + pub fn init(context: *const Context) !Commands { + return .{ + .pending = try context.shared.acquireQueue(), }; } - - pub fn assembleShaderEmbedded(self: *Context, kind: AssemblyKind, comptime path: [:0]const u8, injections: anytype) AssembleError![]const u8 { - return self.assembleShader(kind, path, @embedFile(path), injections); - } - - pub fn deinit(self: *Context) void { - ext.shaderc_compile_options_release(self.spirv_options); - ext.shaderc_compiler_release(self.shader_compiler); - ext.SDL_DestroyGPUDevice(self.gpu_device); - ext.SDL_DestroyWindow(self.window); - self.frame_arena.deinit(); - - self.* = undefined; - } - - pub const traits = ona.Traits{ - .is_thread_unsafe = true, - }; }; pub const Display = struct { @@ -87,209 +29,42 @@ pub const Display = struct { is_hidden: bool, }; -pub const Effect = struct { - pipeline: *ext.SDL_GPUGraphicsPipeline, -}; - -fn compile_shaders(context: ona.Exclusive(Context), _: ona.Assets(Effect)) !void { - const Camera = extern struct { - projection: [4]@Vector(4, f32), - }; - - const vertex_spirv = try context.ptr.assembleShaderEmbedded(.vertex, "./gfx/canvas.vert", .{ - .model_xy = glsl.nativeInput(0, @Vector(2, f32)), - .model_uv = glsl.nativeInput(1, @Vector(2, f32)), - .model_rgba = glsl.nativeInput(2, @Vector(4, f32)), - .instance_uv_offset = glsl.nativeInput(3, @Vector(2, f32)), - .instance_uv_scale = glsl.nativeInput(4, @Vector(2, f32)), - .instance_xbasis = glsl.nativeInput(5, @Vector(2, f32)), - .instance_ybasis = glsl.nativeInput(6, @Vector(2, f32)), - .instance_origin = glsl.nativeInput(7, @Vector(2, f32)), - .instance_pivot = glsl.nativeInput(8, @Vector(2, f32)), - .instance_rgb = glsl.nativeInput(9, @Vector(3, f32)), - .instance_bits = glsl.nativeInput(10, u32), - .uv = glsl.nativeOutput(0, @Vector(2, f32)), - .rgba = glsl.nativeOutput(1, @Vector(4, f32)), - .camera = glsl.nativeUniformBlock(0, Camera), - }); - - const fragment_spirv = try context.ptr.assembleShaderEmbedded(.fragment, "./gfx/canvas.frag", .{ - .vertex_uv = glsl.nativeInput(0, @Vector(2, f32)), - .vertex_rgba = glsl.nativeInput(1, @Vector(4, f32)), - .source_texture = glsl.Sampler{ .sampler_2d = 0 }, - .color = glsl.nativeOutput(0, @Vector(4, f32)), - .camera = glsl.nativeUniformBlock(0, Camera), - }); - - const effect_vertex = ext.SDL_CreateGPUShader(context.ptr.gpu_device, &.{ - .code = vertex_spirv.ptr, - .code_size = vertex_spirv.len, - .format = ext.SDL_GPU_SHADERFORMAT_SPIRV, - .stage = ext.SDL_GPU_SHADERSTAGE_VERTEX, - .entrypoint = "main", - }); - - defer { - ext.SDL_ReleaseGPUShader(context.ptr.gpu_device, effect_vertex); - } - - const effect_fragment = ext.SDL_CreateGPUShader(context.ptr.gpu_device, &.{ - .code = fragment_spirv.ptr, - .code_size = fragment_spirv.len, - .format = ext.SDL_GPU_SHADERFORMAT_SPIRV, - .stage = ext.SDL_GPU_SHADERSTAGE_FRAGMENT, - .entrypoint = "main", - }); - - defer { - ext.SDL_ReleaseGPUShader(context.ptr.gpu_device, effect_fragment); - } - - // _ = try effects.insert(.{ - // .pipeline = ext.SDL_CreateGPUGraphicsPipeline(context.ptr.gpu_device, &.{ - // .vertex_shader = effect_vertex, - // .fragment_shader = effect_fragment, - // .primitive_type = ext.SDL_GPU_PRIMITIVETYPE_TRIANGLELIST, - // }).?, - // }); -} - -pub fn prepare(display: ona.Write(Display)) void { - display.ptr.is_hidden = false; -} - -pub fn render(context: ona.Exclusive(Context)) !void { - const commands = ext.SDL_AcquireGPUCommandBuffer(context.ptr.gpu_device) orelse { - return error.SdlFailure; - }; - - errdefer { - _ = !ext.SDL_CancelGPUCommandBuffer(commands); - } - - var has_swapchain_texture: ?*ext.SDL_GPUTexture = null; - var sawpchain_width, var swapchain_height = [2]u32{ 0, 0 }; - - if (!ext.SDL_WaitAndAcquireGPUSwapchainTexture( - commands, - context.ptr.window, - &has_swapchain_texture, - &sawpchain_width, - &swapchain_height, - )) { - return error.SdlFailure; - } - - if (has_swapchain_texture) |swapchain_texture| { - const color_targets = [_]ext.SDL_GPUColorTargetInfo{ - .{ - .texture = swapchain_texture, - .load_op = ext.SDL_GPU_LOADOP_CLEAR, - .clear_color = .{ .r = 0, .g = 0.2, .b = 0.4, .a = 1.0 }, - }, - }; - - const render_pass = ext.SDL_BeginGPURenderPass(commands, &color_targets, color_targets.len, null) orelse { - return error.SdlFailure; - }; - - defer { - ext.SDL_EndGPURenderPass(render_pass); - } - } - - if (!ext.SDL_SubmitGPUCommandBuffer(commands)) { - return; - } +pub fn queueSynchronize(context: ona.Exclusive(Context), display: ona.Read(Display)) !void { + context.ptr.shared.swapBuffers(display.ptr.*); } pub fn setup(app: *ona.App) !void { - if (!ext.SDL_Init(ext.SDL_INIT_VIDEO)) { + if (!c.SDL_Init(c.SDL_INIT_VIDEO)) { return error.SystemFailure; } - const display = try app.putState(Display{ + const default_display = Display{ .width = 1280, .height = 720, .is_hidden = false, - }); - - const window = ext.SDL_CreateWindow("Ona", display.width, display.height, ext.SDL_WINDOW_HIDDEN) orelse { - return error.SdlFailure; }; - errdefer { - ext.SDL_DestroyWindow(window); - } + try app.setState(default_display); - const gpu_device = ext.SDL_CreateGPUDevice(ext.SDL_GPU_SHADERFORMAT_SPIRV, builtin.mode == .Debug, null) orelse { - return error.SdlFailure; - }; + // var shader_compiler = try glsl.Assembly.init(.spirv, switch (builtin.mode) { + // .Debug, .ReleaseSafe => .unoptimized, + // .ReleaseSmall => .optimize_size, + // .ReleaseFast => .optimize_speed, + // }); - errdefer { - ext.SDL_DestroyGPUDevice(gpu_device); - } + // errdefer { + // shader_compiler.deinit(); + // } - if (!ext.SDL_ClaimWindowForGPUDevice(gpu_device, window)) { - return error.SdlFailure; - } + { + var context = try Context.init(default_display); - const shader_compiler = ext.shaderc_compiler_initialize() orelse { - return error.OutOfMemory; - }; - - errdefer { - ext.shaderc_compiler_release(shader_compiler); - } - - const spirv_options = ext.shaderc_compile_options_initialize() orelse { - return error.OutOfMemory; - }; - - errdefer { - ext.shaderc_compile_options_release(spirv_options); - } - - ext.shaderc_compile_options_set_target_env(spirv_options, ext.shaderc_target_env_vulkan, ext.shaderc_env_version_vulkan_1_1); - - ext.shaderc_compile_options_set_optimization_level(spirv_options, switch (builtin.mode) { - .Debug, .ReleaseSafe => ext.shaderc_optimization_level_zero, - .ReleaseSmall => ext.shaderc_optimization_level_size, - .ReleaseFast => ext.shaderc_optimization_level_performance, - }); - - try ona.registerAsset(app, Effect); - - try app.setState(Context{ - .frame_arena = .init(coral.heap.allocator), - .shader_compiler = shader_compiler, - .spirv_options = spirv_options, - .window = window, - .gpu_device = gpu_device, - }); - - try app.on(.load, .of(prepare)); - try app.on(.load, .of(compile_shaders)); - try app.on(.render, .of(render)); - try app.on(.finish, .of(synchronize)); -} - -pub fn synchronize(context: ona.Exclusive(Context), display: ona.Read(Display)) !void { - const window_flags = ext.SDL_GetWindowFlags(context.ptr.window); - const is_window_hidden = (window_flags & ext.SDL_WINDOW_HIDDEN) != 0; - - if (is_window_hidden != display.ptr.is_hidden) { - const was_window_changed = switch (display.ptr.is_hidden) { - true => ext.SDL_HideWindow(context.ptr.window), - false => ext.SDL_ShowWindow(context.ptr.window), - }; - - if (!was_window_changed) { - std.log.warn("failed to change window visibility", .{}); + errdefer { + context.deinit(); } + + try app.setState(context); } - if (!context.ptr.frame_arena.reset(.{ .retain_with_limit = 1024 * 1024 })) { - std.log.warn("failed to shrink frame arena during reset, freeing instead...", .{}); - } + try app.on(.render, .of(queueSynchronize)); } diff --git a/src/ona/gfx/Context.zig b/src/ona/gfx/Context.zig new file mode 100644 index 0000000..189442d --- /dev/null +++ b/src/ona/gfx/Context.zig @@ -0,0 +1,250 @@ +const builtin = @import("builtin"); + +const c = @cImport({ + @cInclude("SDL3/SDL.h"); +}); + +const coral = @import("coral"); + +const ona = @import("../ona.zig"); + +const std = @import("std"); + +shared: *Shared, +thread: std.Thread, + +pub const Command = union(enum) {}; + +pub const Queue = struct { + commands: coral.stack.Sequential(Command) = .empty, + has_next: ?*Queue = null, + + pub fn cancel(self: *Queue) void { + self.commands.clear(); + } + + fn deinit(self: *Queue) void { + self.commands.deinit(coral.heap.allocator); + } +}; + +const Self = @This(); + +const Shared = struct { + device: *c.SDL_GPUDevice, + display: ona.gfx.Display, + frame_started: std.Thread.ResetEvent = .{}, + frame_finished: std.Thread.ResetEvent = .{}, + has_pooled_queue: std.atomic.Value(?*Queue) = .init(null), + swap_buffers: [2]Buffer = .{ .{}, .{} }, + is_swapped: bool = false, + is_running: bool = true, + + const Buffer = struct { + has_head_queue: ?*Queue = null, + has_tail_queue: ?*Queue = null, + + pub fn dequeue(self: *Buffer) ?*Queue { + if (self.has_head_queue) |head| { + self.has_head_queue = head.has_next; + head.has_next = null; + + if (self.has_head_queue == null) { + self.has_tail_queue = null; + } + + return head; + } + + return null; + } + + pub fn enqueue(self: *Buffer, queue: *Queue) void { + if (self.has_tail_queue) |tail| { + std.debug.assert(self.has_head_queue != null); + + tail.has_next = queue; + self.has_tail_queue = queue; + } else { + self.has_head_queue = queue; + self.has_tail_queue = queue; + } + + queue.has_next = null; + } + }; + + pub fn acquireQueue(self: *Shared) error{OutOfMemory}!*Queue { + const queue = fetch: { + if (self.has_pooled_queue.load(.acquire)) |queue| { + self.has_pooled_queue.store(queue.has_next, .release); + + break :fetch queue; + } + + const queue = try coral.heap.allocator.create(Queue); + + queue.* = .{}; + + break :fetch queue; + }; + + self.swap_buffers[@intFromBool(self.is_swapped)].enqueue(queue); + + return queue; + } + + fn deinit(self: *Shared, thread: std.Thread) void { + self.frame_finished.wait(); + self.frame_finished.reset(); + + self.is_running = false; + + self.frame_started.set(); + thread.join(); + c.SDL_DestroyGPUDevice(self.device); + + while (self.has_pooled_queue.load(.acquire)) |queue| { + self.has_pooled_queue.store(queue.has_next, .release); + coral.heap.allocator.destroy(queue); + } + + for (&self.swap_buffers) |*buffer| { + while (buffer.dequeue()) |queue| { + queue.deinit(); + coral.heap.allocator.destroy(queue); + } + } + } + + fn run(self: *Shared) !void { + const window = c.SDL_CreateWindow("Ona", 1280, 720, c.SDL_WINDOW_HIDDEN) orelse { + return error.SdlFailure; + }; + + defer { + c.SDL_DestroyWindow(window); + } + + if (!c.SDL_ClaimWindowForGPUDevice(self.device, window)) { + return error.SdlFailure; + } + + while (self.is_running) { + const window_flags = c.SDL_GetWindowFlags(window); + const is_window_hidden = (window_flags & c.SDL_WINDOW_HIDDEN) != 0; + + if (is_window_hidden != self.display.is_hidden) { + const was_window_changed = switch (self.display.is_hidden) { + true => c.SDL_HideWindow(window), + false => c.SDL_ShowWindow(window), + }; + + if (!was_window_changed) { + std.log.warn("failed to change window visibility", .{}); + } + } + + const commands = c.SDL_AcquireGPUCommandBuffer(self.device) orelse { + return error.SdlFailure; + }; + + errdefer { + _ = c.SDL_CancelGPUCommandBuffer(commands); + } + + var has_swapchain_texture: ?*c.SDL_GPUTexture = null; + var sawpchain_width, var swapchain_height = [2]u32{ 0, 0 }; + + if (!c.SDL_WaitAndAcquireGPUSwapchainTexture( + commands, + window, + &has_swapchain_texture, + &sawpchain_width, + &swapchain_height, + )) { + return error.SdlFailure; + } + + if (has_swapchain_texture) |swapchain_texture| { + const color_targets = [_]c.SDL_GPUColorTargetInfo{ + .{ + .texture = swapchain_texture, + .load_op = c.SDL_GPU_LOADOP_DONT_CARE, + }, + }; + + const render_pass = c.SDL_BeginGPURenderPass(commands, &color_targets, color_targets.len, null) orelse { + return error.SdlFailure; + }; + + defer { + c.SDL_EndGPURenderPass(render_pass); + } + + while (self.swap_buffers[@intFromBool(!self.is_swapped)].dequeue()) |queue| { + defer { + queue.cancel(); + + queue.has_next = self.has_pooled_queue.load(.acquire); + + self.has_pooled_queue.store(queue, .release); + } + + for (queue.commands.items.slice()) |_| {} + } + } + + _ = c.SDL_SubmitGPUCommandBuffer(commands); + + self.frame_finished.set(); + self.frame_started.wait(); + self.frame_started.reset(); + } + } + + pub fn swapBuffers(self: *Shared, display: ona.gfx.Display) void { + self.frame_finished.wait(); + self.frame_finished.reset(); + + self.display = display; + self.is_swapped = !self.is_swapped; + + self.frame_started.set(); + } +}; + +pub fn init(display: ona.gfx.Display) !Self { + const device = c.SDL_CreateGPUDevice(c.SDL_GPU_SHADERFORMAT_SPIRV, builtin.mode == .Debug, null) orelse { + return error.SdlFailure; + }; + + errdefer { + c.SDL_DestroyGPUDevice(device); + } + + const shared = try coral.heap.allocator.create(Shared); + + errdefer { + coral.heap.allocator.destroy(shared); + } + + shared.* = .{ + .device = device, + .display = display, + }; + + shared.frame_started.set(); + + return .{ + .thread = try .spawn(.{}, Shared.run, .{shared}), + .shared = shared, + }; +} + +pub fn deinit(self: *Self) void { + self.shared.deinit(self.thread); + coral.heap.allocator.destroy(self.shared); + + self.* = undefined; +} diff --git a/src/ona/gfx/composite.frag b/src/ona/gfx/composite.frag new file mode 100644 index 0000000..3131347 --- /dev/null +++ b/src/ona/gfx/composite.frag @@ -0,0 +1,3 @@ +void main() { + color = texture(texture, texCoord); +} \ No newline at end of file diff --git a/src/ona/gfx/composite.vert b/src/ona/gfx/composite.vert new file mode 100644 index 0000000..c626c5a --- /dev/null +++ b/src/ona/gfx/composite.vert @@ -0,0 +1,25 @@ + +const vec2 quadVertices[6] = vec2[]( + 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) +); + +const vec2 quadTexCoords[6] = vec2[]( + vec2(0.0, 1.0), + vec2(0.0, 0.0), + vec2(1.0, 0.0), + + vec2(0.0, 1.0), + vec2(1.0, 0.0), + vec2(1.0, 1.0) +); + +void main() { + gl_Position = vec4(quadVertices[gl_VertexID], 0.0, 1.0); + texCoord = quadTexCoords[gl_VertexID]; +} \ No newline at end of file diff --git a/src/ona/gfx/glsl.zig b/src/ona/gfx/glsl.zig index 0422b20..8f4fcea 100644 --- a/src/ona/gfx/glsl.zig +++ b/src/ona/gfx/glsl.zig @@ -1,9 +1,140 @@ +const c = @cImport({ + @cInclude("shaderc/shaderc.h"); +}); + const coral = @import("coral"); const ona = @import("../ona.zig"); const std = @import("std"); +/// +/// State machine for compiling GLSL shader source code into targeted executable formats. +/// +pub const Assembly = struct { + compiler: c.shaderc_compiler_t, + options: c.shaderc_compile_options_t, + + /// + /// Optimization preference. + /// + pub const Optimizations = enum { + unoptimized, + optimize_speed, + optimize_size, + }; + + /// + /// Target format used for shader binaries. + /// + pub const Target = enum { + spirv, + }; + + /// + /// Assembles and allocates the Ona glsl-encoded `source` into a shader binary using `allocator` as the buffer + /// allocation strategy. + /// + /// `stage` determines how the compiler attempts to interpret and compile the GLSL, with different stages requiring + /// different source code. + /// + /// Failing to compile `source` will result in a errors being logged with what went wrong. It is advised that + /// `name` be supplied a meaningful name to disambiguate which shader the error came from at runtime. In debug + /// builds, a full dump of the problematic shader with the injected code is logged after any error messages. + /// + /// Syntax errors will result in `error.BadSyntax` being returned instead. + /// + /// **Note** the shader assumes the entry-point of the GLSL program matches the signature `void main()` + /// + pub fn assemble( + self: Assembly, + allocator: std.mem.Allocator, + stage: Stage, + name: [*:0]const u8, + source: []const u8, + ) (std.mem.Allocator.Error || error{BadSyntax})![]const u8 { + const result = c.shaderc_compile_into_spv(self.shader_compiler, source.ptr, source.len, switch (stage) { + .fragment => c.shaderc_glsl_fragment_shader, + .vertex => c.shaderc_glsl_vertex_shader, + }, name, "main", self.spirv_options) orelse { + return error.OutOfMemory; + }; + + defer { + c.shaderc_result_release(result); + } + + return switch (c.shaderc_result_get_compilation_status(result)) { + c.shaderc_compilation_status_success => { + const compiled_len = c.shaderc_result_get_length(result); + const compiled_ptr = c.shaderc_result_get_bytes(result); + + return allocator.dupe(u8, compiled_ptr[0..compiled_len]); + }, + + c.shaderc_compilation_status_compilation_error, c.shaderc_compilation_status_invalid_stage => { + std.log.err("{s}", .{c.shaderc_result_get_error_message(result)}); + std.log.debug("problematic shader:\n{s}", .{source}); + + return error.BadSyntax; + }, + + c.shaderc_compilation_status_internal_error => @panic("shaderc internal compiler error"), + else => unreachable, + }; + } + + pub fn deinit(self: *Assembly) void { + c.shaderc_compile_options_release(self.spirv_options); + c.shaderc_compiler_release(self.shader_compiler); + + self.* = undefined; + } + + /// + /// Returns an `Assembly` with `target` as the desired compilation format and `optimization` as the desired + /// optimization level. + /// + /// Should not be enough memory be available, `error.OutOfMemory` is returned instead. + /// + /// **Note** optimizations are not guaranteed and will also depend on the `target` specified. + /// + pub fn init(target: Target, optimizations: Optimizations) error{OutOfMemory}!Assembly { + const compiler = c.shaderc_compiler_initialize() orelse { + return error.OutOfMemory; + }; + + errdefer { + c.shaderc_compiler_release(compiler); + } + + const options = c.shaderc_compile_options_initialize() orelse { + return error.OutOfMemory; + }; + + errdefer { + c.shaderc_compile_options_release(options); + } + + switch (target) { + .spirv => { + c.shaderc_compile_options_set_target_env(options, c.shaderc_target_env_vulkan, c.shaderc_env_version_vulkan_1_1); + }, + } + + c.shaderc_compile_options_set_optimization_level(options, switch (optimizations) { + .unoptimized => c.shaderc_optimization_level_zero, + .optimize_size => c.shaderc_optimization_level_size, + .optimize_speed => c.shaderc_optimization_level_performance, + }); + + return .{ + .compiler = compiler, + .options = options, + }; + } +}; + pub const Field = struct { name: [:0]const u8, primitive: Primitive, @@ -81,6 +212,11 @@ pub const Sampler = union(enum) { } }; +pub const Stage = enum { + vertex, + fragment, +}; + pub const UniformBlock = struct { name: [:0]const u8, binding: usize, diff --git a/src/ona/hid.zig b/src/ona/hid.zig index 6f2f1fb..35cf25a 100644 --- a/src/ona/hid.zig +++ b/src/ona/hid.zig @@ -1,4 +1,4 @@ -const ext = @cImport({ +const c = @cImport({ @cInclude("SDL3/SDL.h"); }); @@ -201,22 +201,22 @@ pub const KeyScancode = enum(u32) { }; pub fn poll(exit: ona.Send(ona.App.Exit), hid_events: ona.Send(ona.hid.Event)) !void { - var event: ext.SDL_Event = undefined; + var event: c.SDL_Event = undefined; - while (ext.SDL_PollEvent(&event)) { + while (c.SDL_PollEvent(&event)) { try hid_events.push(switch (event.type) { - ext.SDL_EVENT_QUIT => { + c.SDL_EVENT_QUIT => { return exit.push(.success); }, - ext.SDL_EVENT_KEY_UP => .{ .key_up = @enumFromInt(event.key.scancode) }, - ext.SDL_EVENT_KEY_DOWN => .{ .key_down = @enumFromInt(event.key.scancode) }, + c.SDL_EVENT_KEY_UP => .{ .key_up = @enumFromInt(event.key.scancode) }, + c.SDL_EVENT_KEY_DOWN => .{ .key_down = @enumFromInt(event.key.scancode) }, - ext.SDL_EVENT_MOUSE_BUTTON_UP => .{ + c.SDL_EVENT_MOUSE_BUTTON_UP => .{ .mouse_up = switch (event.button.button) { - ext.SDL_BUTTON_LEFT => .left, - ext.SDL_BUTTON_RIGHT => .right, - ext.SDL_BUTTON_MIDDLE => .middle, + c.SDL_BUTTON_LEFT => .left, + c.SDL_BUTTON_RIGHT => .right, + c.SDL_BUTTON_MIDDLE => .middle, else => { continue; @@ -224,11 +224,11 @@ pub fn poll(exit: ona.Send(ona.App.Exit), hid_events: ona.Send(ona.hid.Event)) ! }, }, - ext.SDL_EVENT_MOUSE_BUTTON_DOWN => .{ + c.SDL_EVENT_MOUSE_BUTTON_DOWN => .{ .mouse_down = switch (event.button.button) { - ext.SDL_BUTTON_LEFT => .left, - ext.SDL_BUTTON_RIGHT => .right, - ext.SDL_BUTTON_MIDDLE => .middle, + c.SDL_BUTTON_LEFT => .left, + c.SDL_BUTTON_RIGHT => .right, + c.SDL_BUTTON_MIDDLE => .middle, else => { continue; @@ -236,7 +236,7 @@ pub fn poll(exit: ona.Send(ona.App.Exit), hid_events: ona.Send(ona.hid.Event)) ! }, }, - ext.SDL_EVENT_MOUSE_MOTION => .{ + c.SDL_EVENT_MOUSE_MOTION => .{ .mouse_motion = .{ .relative_position = .{ event.motion.xrel, event.motion.yrel }, .absolute_position = .{ event.motion.x, event.motion.y }, @@ -251,6 +251,10 @@ pub fn poll(exit: ona.Send(ona.App.Exit), hid_events: ona.Send(ona.hid.Event)) ! } pub fn setup(app: *ona.App) !void { + if (!c.SDL_Init(c.SDL_INIT_EVENTS)) { + return error.SystemFailure; + } + try app.on(.post_update, .of(poll)); try ona.registerChannel(app, Event); } diff --git a/src/ona/ona.zig b/src/ona/ona.zig index ed92fd6..09dfe60 100644 --- a/src/ona/ona.zig +++ b/src/ona/ona.zig @@ -296,7 +296,6 @@ fn run_realtime_loop(app: *App) !void { try app.run(&tasks, .post_update); try app.run(&tasks, .render); - try app.run(&tasks, .finish); const exit_messages = exit_channel.messages();