From be4972e75e24362b0bcd8a2a4f3422dd0d79bab0 Mon Sep 17 00:00:00 2001 From: kayomn Date: Thu, 18 Sep 2025 19:07:29 +0100 Subject: [PATCH] Add graphics device resources --- .gitattributes | 1 + .vscode/launch.json | 11 + .zigversion | 2 +- build.zig | 32 +- build.zig.zon | 8 +- src/coral/Tasks.zig | 103 ----- src/coral/bytes.zig | 167 ++++---- src/coral/coral.zig | 47 --- src/coral/heap.zig | 75 +--- src/coral/meta.zig | 24 +- src/coral/stack.zig | 4 +- src/coral/tree.zig | 117 +++--- src/demos/crt.frag | 22 ++ src/demos/graphics.eff | 20 - src/demos/graphics.zig | 119 +++--- src/demos/graphics_image.qoi | Bin 0 -> 15750 bytes src/ona/App.zig | 152 +++++++- src/ona/{App => }/Setup.zig | 14 +- src/ona/{App/Behavior.zig => System.zig} | 11 +- src/ona/{App => }/SystemGraph.zig | 46 +-- src/ona/asset.zig | 293 ++++++++++++--- src/ona/gfx.zig | 460 ++++++++++++++++++++--- src/ona/gfx/Color.zig | 34 ++ src/ona/gfx/Context.zig | 250 ------------ src/ona/gfx/canvas.vert | 8 +- src/ona/gfx/composite.frag | 3 - src/ona/gfx/composite.vert | 25 -- src/ona/gfx/device.zig | 435 +++++++++++++++++++++ src/ona/gfx/glsl.zig | 45 ++- src/ona/gfx/qoi.zig | 177 +++++++++ src/ona/ona.zig | 99 ++--- src/ona/tasks.zig | 192 ++++++++++ src/{coral/Tasks => ona/tasks}/Queue.zig | 39 +- 33 files changed, 2112 insertions(+), 923 deletions(-) delete mode 100644 src/coral/Tasks.zig create mode 100644 src/demos/crt.frag delete mode 100644 src/demos/graphics.eff create mode 100644 src/demos/graphics_image.qoi rename src/ona/{App => }/Setup.zig (88%) rename src/ona/{App/Behavior.zig => System.zig} (97%) rename src/ona/{App => }/SystemGraph.zig (86%) create mode 100644 src/ona/gfx/Color.zig delete mode 100644 src/ona/gfx/Context.zig delete mode 100644 src/ona/gfx/composite.frag delete mode 100644 src/ona/gfx/composite.vert create mode 100644 src/ona/gfx/device.zig create mode 100644 src/ona/gfx/qoi.zig create mode 100644 src/ona/tasks.zig rename src/{coral/Tasks => ona/tasks}/Queue.zig (76%) diff --git a/.gitattributes b/.gitattributes index f94efde..65fdbdf 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ *.bmp filter=lfs diff=lfs merge=lfs -text *.png filter=lfs diff=lfs merge=lfs -text *.spv filter=lfs diff=lfs merge=lfs -text +*.qoi filter=lfs diff=lfs merge=lfs -text diff --git a/.vscode/launch.json b/.vscode/launch.json index 9549ba8..9ea196b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,6 +11,17 @@ "preLaunchTask": "Build" }, + { + "name": "Debug Qoi", + "type": "gdb", + "request": "launch", + "target": "${workspaceRoot}/qoi-master/qoiconv", + "arguments": "${workspaceRoot}/src/demos/graphics_image.qoi ${workspaceRoot}/test.qoi", + "cwd": "${workspaceRoot}/src/demos/", + "valuesFormatting": "parseText", + "preLaunchTask": "Build" + }, + { "name": "Debug Graphics Demo", "type": "gdb", diff --git a/.zigversion b/.zigversion index 0548fb4..8076af5 100644 --- a/.zigversion +++ b/.zigversion @@ -1 +1 @@ -0.14.0 \ No newline at end of file +0.15.1 \ No newline at end of file diff --git a/build.zig b/build.zig index 56e3720..12ab3bf 100644 --- a/build.zig +++ b/build.zig @@ -4,6 +4,7 @@ const BuildConfig = struct { module_target: std.Build.ResolvedTarget, spirv_target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, + sdl_dependency: *std.Build.Dependency, fn scan_demos(self: BuildConfig, ona_module: *std.Build.Module) void { const b = ona_module.owner; @@ -53,10 +54,9 @@ const BuildConfig = struct { }), }); - demo_executable.linkSystemLibrary2("SDL3", .{ - .needed = true, - .preferred_link_mode = .dynamic, - }); + demo_executable.use_llvm = true; + + demo_executable.linkLibrary(self.sdl_dependency.artifact("SDL3")); const demo_installation = b.addInstallArtifact(demo_executable, .{}); @@ -94,9 +94,21 @@ const CommonArgs = struct { }; pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + + const optimize = b.standardOptimizeOption(.{ + .preferred_optimize_mode = .Debug, + }); + const config = BuildConfig{ - .optimize = b.standardOptimizeOption(.{}), - .module_target = b.standardTargetOptions(.{}), + .optimize = optimize, + .module_target = target, + + .sdl_dependency = b.dependency("sdl", .{ + .target = target, + .optimize = optimize, + .preferred_linkage = .static, + }), .spirv_target = b.resolveTargetQuery(.{ .cpu_arch = .spirv64, @@ -109,12 +121,6 @@ pub fn build(b: *std.Build) void { const shaderc_dependency = b.dependency("shaderc_zig", .{}); - const sdl_dependency = b.dependency("sdl", .{ - .target = config.module_target, - .optimize = config.optimize, - .preferred_linkage = .static, - }); - const coral_module = b.addModule("coral", .{ .root_source_file = b.path("src/coral/coral.zig"), .target = config.module_target, @@ -136,7 +142,7 @@ pub fn build(b: *std.Build) void { }); ona_module.linkLibrary(shaderc_dependency.artifact("shaderc")); - ona_module.linkLibrary(sdl_dependency.artifact("SDL3")); + ona_module.linkLibrary(config.sdl_dependency.artifact("SDL3")); // config.addShaders(ona_module, &.{ // "./src/ona/gfx/effect_shader.zig", diff --git a/build.zig.zon b/build.zig.zon index 5c8fca4..30c36c8 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -4,12 +4,12 @@ .dependencies = .{ .shaderc_zig = .{ - .url = "git+https://github.com/tiawl/shaderc.zig#06565d2af3beec9780b11524984211ebd104fd21", - .hash = "shaderc_zig-1.0.0-mOl840tjAwBiAnMSfRskq0Iq3JJ9jPRHy2JoEgnUvSpV", + .url = "git+https://github.com/tiawl/shaderc.zig#69b67221988aa84c91447775ad6157e4e80bab00", + .hash = "shaderc_zig-1.0.0-mOl846VjAwDV8YlqQFVvFsWsBa6dLNSiskpTy7lC1hmD", }, .sdl = .{ - .url = "git+https://github.com/castholm/SDL.git#0f81c0affb2584b242b2fb5744e7dfebcfd904a5", - .hash = "sdl-0.2.6+3.2.20-7uIn9JkjfwGIQ6j3-etow2rCe-Zt16Yj-2gdp9jW7WZ9", + .url = "git+https://github.com/castholm/SDL.git#b1913e7c31ad72ecfd3ab04aeac387027754cfaf", + .hash = "sdl-0.3.0+3.2.22-7uIn9Pg3fwGG2IyIOPxxOSVe-75nUng9clt7tXGFLzMr", }, }, diff --git a/src/coral/Tasks.zig b/src/coral/Tasks.zig deleted file mode 100644 index 036e197..0000000 --- a/src/coral/Tasks.zig +++ /dev/null @@ -1,103 +0,0 @@ -const Queue = @import("./Tasks/Queue.zig"); - -const builtin = @import("builtin"); - -const coral = @import("./coral.zig"); - -const std = @import("std"); - -prioritised_tasks: [priorities.len]?*Queue = [_]?*Queue{null} ** priorities.len, - -pub const Priority = enum { - high_priority, - low_priority, - background, - - fn getThreadCount(self: Priority, cpu_count: usize) usize { - const cpu_share: f64 = @floatFromInt(cpu_count); - - return @intFromFloat(switch (self) { - .high_priority => cpu_share * 0.375, - .low_priority => cpu_share * 0.25, - .background => cpu_share * 0.125, - }); - } -}; - -const Self = @This(); - -pub fn create(self: *Self, allocator: std.mem.Allocator, priority: Priority, task: anytype) std.mem.Allocator.Error!*@TypeOf(task) { - const Task = @TypeOf(task); - - const run_fn = coral.meta.hasFn(Task, "run") orelse { - @compileError(std.fmt.comptimePrint("{s} requires a .run fn to be a valid task type", .{@typeName(Task)})); - }; - - if (run_fn.return_type.? != void) { - @compileError(std.fmt.comptimePrint("{s}.run fn must return a void type", .{@typeName(Task)})); - } - - if (run_fn.params.len != 1 or run_fn.params[0].type != *Task) { - @compileError(std.fmt.comptimePrint("{s}.run fn must accept a {s} as it's one and only parameter, not {s}", .{ - @typeName(Task), - @typeName(*Task), - if (run_fn.params[0].type) |Type| @typeName(Type) else "anytype", - })); - } - - const created_task = try allocator.create(Task); - - created_task.* = task; - - if (self.prioritised_tasks[@intFromEnum(priority)]) |tasks| { - tasks.enqueue(.initRef(created_task, Task.run)); - } else { - created_task.run(); - } - - return created_task; -} - -pub fn finish(self: *Self, priority: Priority) void { - if (self.prioritised_tasks[@intFromEnum(priority)]) |tasks| { - tasks.finish(); - } -} - -const priorities = std.enums.values(Priority); - -pub fn start(self: *Self) std.Thread.SpawnError!void { - self.* = .{}; - - if (!builtin.single_threaded) { - if (std.Thread.getCpuCount()) |cpu_count| { - errdefer { - self.stop(); - } - - inline for (priorities, &self.prioritised_tasks) |priority, *has_tasks| { - const thread_count = priority.getThreadCount(cpu_count); - - if (thread_count != 0) { - has_tasks.* = try Queue.spawn(thread_count); - } - } - } else |cpu_count_error| { - std.log.warn("Failed to get number of CPU cores available: {s}", .{@errorName(cpu_count_error)}); - } - } -} - -pub fn stop(self: *Self) void { - inline for (&self.prioritised_tasks) |*has_tasks| { - if (has_tasks.*) |tasks| { - tasks.stop(); - } - - has_tasks.* = null; - } - - for (priorities) |priority| { - self.finish(priority); - } -} diff --git a/src/coral/bytes.zig b/src/coral/bytes.zig index d47809f..6f1440f 100644 --- a/src/coral/bytes.zig +++ b/src/coral/bytes.zig @@ -13,11 +13,17 @@ pub const ReadOnlySpan = struct { read_cursor: usize = 0, pub fn read(self: *ReadOnlySpan, buffer: []u8) usize { - _ = self; - _ = buffer; + const bytes_read = @min(buffer.len, self.bytes.len - self.read_cursor); - // TODO: Implement. - unreachable; + @memcpy(buffer[0..bytes_read], self.bytes[self.read_cursor .. self.read_cursor + bytes_read]); + + self.read_cursor += bytes_read; + + return bytes_read; + } + + pub fn reader(self: *ReadOnlySpan) Readable { + return .initRef(self, read); } }; @@ -30,14 +36,6 @@ pub const ReadWriteSpan = struct { write_cursor: usize = 0, read_cursor: usize = 0, - pub fn read(self: *ReadWriteSpan, buffer: []u8) usize { - _ = self; - _ = buffer; - - // TODO: Implement. - unreachable; - } - pub fn put(self: *ReadWriteSpan, byte: u8) bool { if (self.write_cursor >= self.bytes.len) { return false; @@ -49,14 +47,28 @@ pub const ReadWriteSpan = struct { return true; } + pub fn read(self: *ReadWriteSpan, buffer: []u8) usize { + const bytes_read = @min(buffer.len, self.bytes.len - self.read_cursor); + + @memcpy(buffer[0..bytes_read], self.bytes[self.read_cursor .. self.read_cursor + bytes_read]); + + self.read_cursor += bytes_read; + + return bytes_read; + } + + pub fn reader(self: *ReadWriteSpan) Readable { + return .initRef(self, read); + } + pub fn write(self: *ReadWriteSpan, buffer: []const u8) usize { - const written = @min(buffer.len, self.bytes.len - self.write_cursor); + const bytes_written = @min(buffer.len, self.bytes.len - self.write_cursor); - @memcpy(self.bytes[self.write_cursor .. self.write_cursor + written], buffer[0..written]); + @memcpy(self.bytes[self.write_cursor .. self.write_cursor + bytes_written], buffer[0..bytes_written]); - self.write_cursor += written; + self.write_cursor += bytes_written; - return written; + return bytes_written; } pub fn writer(self: *ReadWriteSpan) Writable { @@ -128,7 +140,7 @@ pub fn printFormatted(buffer: [:0]u8, comptime format: []const u8, args: anytype return error.BufferOverflow; } - var buffer_span = span(buffer); + var buffer_span = spanOf(buffer); writeFormatted(buffer_span.writer(), format, args) catch unreachable; @@ -139,40 +151,56 @@ pub fn printFormatted(buffer: [:0]u8, comptime format: []const u8, args: anytype return buffer[0..len :0]; } -pub fn readBig(input: Readable, comptime T: type) ReadWriteError!T { - var buffer: [@sizeOf(T)]u8 align(@alignOf(T)) = undefined; - - if (input.call(.{&buffer}) != buffer.len) { +pub fn readAll(input: Readable, buffer: []u8) ReadWriteError!void { + if (input.call(.{buffer}) != buffer.len) { return error.IncompleteRead; } - - switch (builtin.cpu.arch.endian()) { - .big => {}, - - .little => { - std.mem.byteSwapAllFields(T, @ptrCast(&buffer)); - }, - } - - return std.mem.bytesToValue(T, &buffer); } -pub fn readLittle(input: Readable, comptime T: type) ReadWriteError!T { - var buffer: [@sizeOf(T)]u8 = undefined; +pub fn readBig(input: Readable, comptime Value: type) ReadWriteError!Value { + var buffer: [@sizeOf(Value)]u8 align(@alignOf(Value)) = undefined; - if (input.call(.{&buffer}) != buffer.len) { + try readAll(input, &buffer); + + if (@sizeOf(Value) > 1) { + switch (builtin.cpu.arch.endian()) { + .little => { + std.mem.reverse(u8, &buffer); + }, + + .big => {}, + } + } + + return std.mem.bytesToValue(Value, &buffer); +} + +pub fn readByte(input: Readable) ReadWriteError!u8 { + var byte: u8 = undefined; + + if (input.call(.{std.mem.asBytes(&byte)}) != 1) { return error.IncompleteRead; } - switch (builtin.cpu.arch.endian()) { - .big => { - std.mem.byteSwapAllFields(T, &buffer); - }, + return byte; +} - .little => {}, +pub fn readLittle(input: Readable, comptime Value: type) ReadWriteError!Value { + var buffer: [@sizeOf(Value)]u8 align(@alignOf(Value)) = undefined; + + try readAll(input, &buffer); + + if (@sizeOf(Value) > 1) { + switch (builtin.cpu.arch.endian()) { + .little => {}, + + .big => { + std.mem.reverse(u8, &buffer); + }, + } } - return std.mem.bytesToValue(T, &buffer); + return std.mem.bytesToValue(Value, &buffer); } pub const readNative = switch (builtin.cpu.arch.endian()) { @@ -180,7 +208,7 @@ pub const readNative = switch (builtin.cpu.arch.endian()) { .big => readBig, }; -pub fn span(ptr: anytype) Span(@TypeOf(ptr)) { +pub fn spanOf(ptr: anytype) Span(@TypeOf(ptr)) { return .{ .bytes = switch (@typeInfo(@TypeOf(ptr)).pointer.size) { .slice => std.mem.sliceAsBytes(ptr), @@ -250,11 +278,21 @@ pub fn writeAll(output: Writable, data: []const u8) ReadWriteError!void { } } -pub fn writeN(output: Writable, data: []const u8, count: usize) ReadWriteError!void { - var remaining = count; +pub fn writeBig(output: Writable, value: anytype) ReadWriteError!void { + switch (builtin.cpu.arch.endian()) { + .little => { + var buffer = std.mem.toBytes(value); - while (remaining != 0) : (remaining -= 1) { - try writeAll(output, data); + if (@sizeOf(@TypeOf(value)) > 1) { + std.mem.reverse(u8, &buffer); + } + + try writeAll(output, &buffer); + }, + + .big => { + try writeAll(output, std.mem.asBytes(&value)); + }, } } @@ -376,33 +414,30 @@ pub fn writeLittle(output: Writable, value: anytype) ReadWriteError!void { }, .big => { - const Value = @TypeOf(value); + var buffer = std.mem.toBytes(value); - switch (@typeInfo(Value)) { - .@"struct", .array => { - var copy = value; - - std.mem.byteSwapAllFields(Value, ©); - - try writeAll(output, std.mem.asBytes(©)); - }, - - .int, .float, .bool => { - try writeAll(output, std.mem.asBytes(&@byteSwap(value))); - }, - - .@"enum" => { - try writeLittle(output, @intFromEnum(value)); - }, - - else => { - @compileError(std.fmt.comptimePrint("{s} is not byte-swappable", .{@typeName(Value)})); - }, + if (@sizeOf(@TypeOf(value)) > 1) { + std.mem.reverse(u8, &buffer); } + + try writeAll(output, &buffer); }, } } +pub fn writeN(output: Writable, data: []const u8, count: usize) ReadWriteError!void { + var remaining = count; + + while (remaining != 0) : (remaining -= 1) { + try writeAll(output, data); + } +} + +pub const writeNative = switch (builtin.cpu.arch.endian()) { + .little => writeLittle, + .big => writeBig, +}; + fn writeNull(buffer: []const u8) usize { return buffer.len; } diff --git a/src/coral/coral.zig b/src/coral/coral.zig index 2a2a20b..1c61444 100644 --- a/src/coral/coral.zig +++ b/src/coral/coral.zig @@ -1,7 +1,5 @@ pub const Box = @import("./Box.zig"); -const Tasks = @import("./Tasks.zig"); - pub const TypeId = @import("./TypeId.zig"); pub const bytes = @import("./bytes.zig"); @@ -24,49 +22,6 @@ pub const tree = @import("./tree.zig"); pub const utf8 = @import("./utf8.zig"); -pub fn CallTask(comptime function: anytype) type { - const Function = @TypeOf(function); - - const function_fn = switch (@typeInfo(Function)) { - .@"fn" => |@"fn"| @"fn", - else => @compileError("expeceted param `function` to be an fn type"), - }; - - return struct { - args: std.meta.ArgsTuple(Function), - payload: Return = undefined, - resolved: std.Thread.ResetEvent = .{}, - - const Return = function_fn.return_type.?; - - const Self = @This(); - - pub fn get(self: *const Self) Return { - std.debug.assert(self.resolved.isSet()); - self.resolved.wait(); - self.resolved.reset(); - - return self.payload; - } - - pub fn resolve(self: *Self, payload: Return) bool { - if (self.resolved.isSet()) { - return false; - } - - self.payload = payload; - - self.resolved.set(); - - return true; - } - - pub fn run(self: *Self) void { - std.debug.assert(self.resolve(@call(.auto, function, self.args))); - } - }; -} - pub fn Callable(comptime Output: type, comptime input_types: []const type) type { const InputTuple = std.meta.Tuple(input_types); @@ -178,5 +133,3 @@ pub fn expect(function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) me else => @call(.auto, function, args), }; } - -pub var tasks = Tasks{}; diff --git a/src/coral/heap.zig b/src/coral/heap.zig index a73c3d5..59d531e 100644 --- a/src/coral/heap.zig +++ b/src/coral/heap.zig @@ -2,74 +2,19 @@ const builtin = @import("builtin"); const std = @import("std"); -pub fn RefCounting(comptime Payload: type, comptime finalize: fn (*Payload) void) type { - const AtomicCount = std.atomic.Value(usize); - - return opaque { - const Layout = struct { - ref_count: AtomicCount, - payload: Payload, - - fn get(self: *Self) *Layout { - return @ptrCast(@alignCast(self)); - } - - fn get_const(self: *const Self) *const Layout { - return @ptrCast(@alignCast(self)); - } - }; - - const Self = @This(); - - pub fn acquire(self: *Self) *const Payload { - const layout = Layout.get(self); - const ref_count = layout.ref_count.fetchAdd(1, .monotonic); - - std.debug.assert(ref_count != 0); - - return &layout.payload; - } - - pub fn create(payload: Payload) std.mem.Allocator.Error!*Self { - const allocation = try allocator.create(Layout); - - errdefer { - allocator.destroy(allocation); - } - - allocation.* = .{ - .ref_count = AtomicCount.init(1), - .payload = payload, - }; - - return @ptrCast(allocation); - } - - pub fn release(self: *Self) void { - const layout = Layout.get(self); - const ref_count = layout.ref_count.fetchSub(1, .monotonic); - - std.debug.assert(ref_count != 0); - - if (ref_count == 1) { - finalize(&layout.payload); - allocator.destroy(layout); - } - } - - pub fn weak(self: *const Self) *const Payload { - return &Layout.get_const(self).payload; - } - }; -} - pub const allocator = switch (builtin.mode) { - .ReleaseFast => std.heap.smp_allocator, - else => gpa.allocator(), + .ReleaseFast, .ReleaseSmall => std.heap.smp_allocator, + else => debug_allocator.allocator(), }; -var gpa = std.heap.GeneralPurposeAllocator(.{ .thread_safe = true }){}; +var debug_allocator = switch (builtin.mode) { + .ReleaseFast, .ReleaseSmall => {}, + else => std.heap.DebugAllocator(.{}){}, +}; pub fn traceLeaks() void { - _ = gpa.detectLeaks(); + switch (builtin.mode) { + .ReleaseFast, .ReleaseSmall => {}, + else => _ = debug_allocator.detectLeaks(), + } } diff --git a/src/coral/meta.zig b/src/coral/meta.zig index 7d8e447..57998bc 100644 --- a/src/coral/meta.zig +++ b/src/coral/meta.zig @@ -22,13 +22,6 @@ pub fn UnwrappedOptional(comptime Value: type) type { }; } -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); @@ -47,3 +40,20 @@ pub fn hasFn(comptime Type: type, comptime fn_name: []const u8) ?std.builtin.Typ else => null, }; } + +pub fn isContainer(@"type": std.builtin.Type) bool { + return switch (@"type") { + .@"struct", .@"union", .@"enum", .@"opaque" => true, + else => false, + }; +} + +pub fn parametersMatch(parameters: []const std.builtin.Type.Fn.Param, types: []const type) bool { + for (parameters, types) |parameter, Type| { + if (parameter.type != Type) { + return false; + } + } + + return true; +} diff --git a/src/coral/stack.zig b/src/coral/stack.zig index 156181d..897a229 100644 --- a/src/coral/stack.zig +++ b/src/coral/stack.zig @@ -118,11 +118,11 @@ pub fn Parallel(comptime Item: type) type { else => @compileError("`Item` must be a struct type"), }; - const item_align = @alignOf(Item); + const item_align = std.mem.Alignment.of(Item); const item_size = @sizeOf(Item); return Generic(Item, struct { - ptr: [*]align(item_align) u8 = undefined, + ptr: [*]align(item_align.toByteUnits()) u8 = undefined, len: u32 = 0, cap: u32 = 0, diff --git a/src/coral/tree.zig b/src/coral/tree.zig index 756be79..aa51252 100644 --- a/src/coral/tree.zig +++ b/src/coral/tree.zig @@ -66,67 +66,6 @@ pub fn Binary(comptime Key: type, comptime Value: type, comptime traits: Traits( return node; } - - fn remove(self: *Node, key: Key) ?*Node { - const node = self.find(key) orelse { - return null; - }; - - if (node.has_lesser == null) { - std.debug.assert(self.transplant(node.has_greater)); - } else if (node.has_greater == null) { - std.debug.assert(self.transplant(node.has_lesser)); - } else { - var successor = node.has_greater.?.getMin(); - - if (successor.has_parent != node) { - // Move successor up: replace successor with its right child first - std.debug.assert(successor.transplant(successor.has_greater)); - - // Attach node.right to successor - successor.has_greater = node.has_greater; - - if (successor.has_greater) |g| { - g.has_parent = successor; - } - } - - // Replace node with successor - std.debug.assert(node.transplant(successor)); - - // Attach node.left to successor - successor.has_lesser = node.has_lesser; - - if (successor.has_lesser) |l| { - l.has_parent = successor; - } - } - - // Detach the removed node completely and return it - node.has_parent = null; - node.has_lesser = null; - node.has_greater = null; - - return node; - } - - fn transplant(self: *Node, has_node: ?*Node) bool { - const parent = self.has_parent orelse { - return false; - }; - - if (parent.has_lesser == self) { - parent.has_lesser = has_node; - } else { - parent.has_greater = has_node; - } - - if (has_node) |node| { - node.has_parent = self.has_parent; - } - - return true; - } }; pub const KeyValues = struct { @@ -330,10 +269,45 @@ pub fn Binary(comptime Key: type, comptime Value: type, comptime traits: Traits( return null; }; - const node = root.remove(key) orelse { + const node = root.find(key) orelse { return null; }; + if (node.has_lesser == null) { + self.transplant(node, node.has_greater); + } else if (node.has_greater == null) { + self.transplant(node, node.has_lesser); + } else { + // Successor = min(node.right) + var successor = node.has_greater.?.getMin(); + + if (successor.has_parent != node) { + // move successor up (replace successor with its right child) + self.transplant(successor, successor.has_greater); + + // attach node.right to successor + successor.has_greater = node.has_greater; + + if (successor.has_greater) |g| { + g.has_parent = successor; + } + } + + // replace node with successor + self.transplant(node, successor); + + // attach node.left to successor + successor.has_lesser = node.has_lesser; + + if (successor.has_lesser) |l| { + l.has_parent = successor; + } + } + + node.has_parent = null; + node.has_lesser = null; + node.has_greater = null; + defer { self.free_nodes.destroy(node); } @@ -343,6 +317,25 @@ pub fn Binary(comptime Key: type, comptime Value: type, comptime traits: Traits( .value = node.value, }; } + + fn transplant(self: *Self, u: *Node, has_v: ?*Node) void { + const u_parent = u.has_parent orelse { + // U is root. + self.has_root = has_v; + + return; + }; + + if (u_parent.has_lesser == u) { + u_parent.has_lesser = has_v; + } else { + u_parent.has_greater = has_v; + } + + if (has_v) |v| { + v.has_parent = u.has_parent; + } + } }; } diff --git a/src/demos/crt.frag b/src/demos/crt.frag new file mode 100644 index 0000000..4fce957 --- /dev/null +++ b/src/demos/crt.frag @@ -0,0 +1,22 @@ + +vec2 crt(vec2 coords, float bend) { + vec2 symmetrical_coords = (coords - 0.5) * 2.0; + + vec2 deformed_coords = symmetrical_coords * (symmetrical_coords + vec2( + pow((abs(coords.y) / bend), 2.0), + pow((abs(coords.x) / bend), 2.0) + )); + + return (deformed_coords / 2.0) + 0.5; +} + +void main() { + const vec2 crt_coords = crt(vertex_uv, 4.8); + const float scanlineCount = 480.0; + const float scan = sin(crt_coords.y * scanlineCount + effect.time * 29.0); + + const vec3 image_color = texture(image, crt_coords).rgb; + const vec3 shaded = image_color - vec3(scan * 0.02); + + color = vec4(image_color, 1.0); +} \ No newline at end of file diff --git a/src/demos/graphics.eff b/src/demos/graphics.eff deleted file mode 100644 index 9f0b446..0000000 --- a/src/demos/graphics.eff +++ /dev/null @@ -1,20 +0,0 @@ - -function crt(coords float2, bend float) -> float2 { - let symmetrical_coords = (coords - 0.5) * 2.0 - - let deformed_coords = symmetrical_coords * (symmetrical_coords + float2( - pow((abs(coords.y) / bend), 2.0), - pow((abs(coords.x) / bend), 2.0) - )) - - return (deformed_coords / 2.0) + 0.5 -} - -function frag(color float4, uv float2) -> float4 { - let crt_coords = crt(uv, 4.8) - let screenspace = crt_coords * float2(effect.screen_width, effect.screen_height) - let color = sample(albedo, crt_coords).rgb - let scanline = float4((color - sin((screenspace.y + (effect.time * 29.0))) * 0.02), 1.0) - - return scanline -} diff --git a/src/demos/graphics.zig b/src/demos/graphics.zig index 8f61346..e98b603 100644 --- a/src/demos/graphics.zig +++ b/src/demos/graphics.zig @@ -9,65 +9,90 @@ const CRT = extern struct { padding: [4]u8 = undefined, }; -const Effects = struct { - render_texture: ona.AssetHandle = .default, - image_textures: [2]ona.AssetHandle = [_]ona.AssetHandle{.default} ** 2, +const State = struct { + images: [2]ona.gfx.Images.Handle = [_]ona.gfx.Images.Handle{.default} ** 2, last_time: f64 = 0, image_index: usize = 0, - crt_effect: ona.AssetHandle = .default, + crt_effect: ona.gfx.Effects.Handle = .default, + loaded_image: ona.gfx.Images.Handle = .default, }; +fn load(display: ona.Write(ona.gfx.Display), state: ona.Write(State), images: ona.gfx.Images, effects: ona.gfx.Effects) !void { + display.ptr.size = .{ 1280, 720 }; + + state.ptr.loaded_image = try images.load(ona.gfx.QoiImage{ + .path = try .parse("graphics_image.qoi"), + }); + + errdefer { + images.unload(state.ptr.images[1]); + } + + state.ptr.crt_effect = try effects.load(ona.gfx.GlslEffect{ + .fragment = .{ + .glsl = @embedFile("crt.frag"), + .name = "crt.frag", + }, + }); + + errdefer { + effects.unload(state.ptr.crt_effect); + } + + state.ptr.images[0] = try images.load(ona.gfx.CheckerImage{ + .colors = .{ .black, .purple }, + .square_size = 4, + .width = 8, + .height = 8, + }); + + errdefer { + images.unload(state.ptr.images[0]); + } + + state.ptr.images[1] = try images.load(ona.gfx.CheckerImage{ + .colors = .{ .black, .grey }, + .square_size = 4, + .width = 8, + .height = 8, + }); + + errdefer { + images.unload(state.ptr.images[1]); + } +} + pub fn main() void { ona.realtime_app .with(.initModule(ona.hid)) .with(.initModule(ona.gfx)) - // .with(.initSystem(.render, .of(render))) + .with(.initState(State{})) + .with(.initSystem(.load, .of(load))) + .with(.initSystem(.render, .of(render))) + .with(.initSystem(.update, .of(update))) .run(); } -// fn update(effects: ona.Write(Effects), loop: ona.Read(ona.Loop)) void { -// const update_seconds = 5; +fn render(scene: ona.gfx.Scene, state: ona.Read(State), display: ona.Write(ona.gfx.Display), time: ona.Read(ona.App.Time)) void { + const width, const height = display.ptr.size; -// if ((loop.state.elapsed_time - effects.state.last_time) > update_seconds) { -// effects.state.image_index = (effects.state.image_index + 1) % effects.state.image_textures.len; -// effects.state.last_time = loop.state.elapsed_time; -// } -// } + scene.updateEffect(state.ptr.crt_effect, CRT{ + .width = @floatFromInt(width), + .height = @floatFromInt(height), + .time = @floatCast(time.ptr.elapsed), + }); -// fn render(commands: ona.gfx.Commands) void { -// try commands.setTarget(.{ -// .texture = effects.state.render_texture, -// .clear_color = gfx.colors.black, -// .clear_depth = 0, -// .clear_stencil = 0, -// }); + scene.drawSprite(state.ptr.images[state.ptr.image_index], .{ + .size = .{ @floatFromInt(width), @floatFromInt(height) }, + .effect = state.ptr.crt_effect, + }); +} -// const width: f32 = @floatFromInt(display.state.width); -// const height: f32 = @floatFromInt(display.state.height); +fn update(state: ona.Write(State), time: ona.Read(ona.App.Time)) void { + const update_seconds = 5; -// try commands.draw_texture(.{ -// .texture = effects.state.image_textures[effects.state.image_index], -// .size = .{ width, height }, -// }); - -// try commands.set_effect(.{ -// .effect = effects.state.crt_effect, - -// .properties = std.mem.asBytes(&CRT{ -// .width = width, -// .height = height, -// .time = @floatCast(loop.state.elapsed_time), -// }), -// }); - -// try commands.set_target(.{ -// .clear_color = null, -// .clear_depth = null, -// .clear_stencil = null, -// }); - -// try commands.draw_texture(.{ -// .texture = effects.state.render_texture, -// .size = .{ width, height }, -// }); -// } + if ((time.ptr.elapsed - state.ptr.last_time) > update_seconds) { + state.ptr.image_index = (state.ptr.image_index + 1) % state.ptr.images.len; + state.ptr.last_time = time.ptr.elapsed; + } +} diff --git a/src/demos/graphics_image.qoi b/src/demos/graphics_image.qoi new file mode 100644 index 0000000000000000000000000000000000000000..9485f8c4211a8d8e4a512b83712205d39cbb05a3 GIT binary patch literal 15750 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 self.opened_storages.len) { + return error.SystemResources; + } + + const storage = &self.opened_storages[self.storages_opened]; + + storage.* = c.SDL_OpenTitleStorage(native_path, 0) orelse { + return error.SystemResources; + }; + + self.storages_opened += 1; + + while (!c.SDL_StorageReady(storage.*)) { + c.SDL_Delay(1); + } + } + + pub fn readFile(self: Data, path: Path, output: []u8) AccessError!void { + for (self.storages[0..self.storages_opened]) |storage| { + if (c.SDL_ReadStorageFile(storage, path.ptr(), @ptrCast(output.ptr), output.len)) { + return; + } + } + + return error.SystemResources; + } + + pub fn sizeFile(self: Data, path: Path) AccessError!usize { + var file_size: u64 = 0; + + for (self.storages[0..self.storages_opened]) |storage| { + if (c.SDL_GetStorageFileSize(storage, path.ptr(), &file_size)) { + return file_size; + } + } + + return error.SystemResources; + } +}; + pub const Exit = union(enum) { success, failure: anyerror, }; +pub const Path = struct { + buffer: [max]u8, + unused: u8, + + pub const ParseError = error{ + BadPath, + }; + + pub const empty = Path{ + .buffer = std.mem.zeroes([max]u8), + .unused = max, + }; + + const max = std.math.maxInt(u8); + + pub fn parse(data: []const u8) ParseError!Path { + if (data.len == 0) { + return error.BadPath; + } + + if (data.len > max) { + return error.BadPath; + } + + var path = empty; + + path.unused -= @intCast(data.len); + + @memcpy(path.buffer[0..data.len], data); + + return path; + } + + pub fn ptr(self: *const Path) [*:0]const u8 { + return @ptrCast(&self.buffer); + } +}; + pub const RunError = std.mem.Allocator.Error || error{ MissingDependency, }; @@ -25,7 +141,7 @@ pub const RunError = std.mem.Allocator.Error || error{ const Self = @This(); pub const Time = struct { - elapsed: f64, + elapsed: f64 = 0, }; pub fn deinit(self: *Self) void { @@ -57,7 +173,7 @@ fn scheduleName(comptime schedule: anytype) [:0]const u8 { }; } -pub fn hasState(self: *const Self, comptime State: type) ?*State { +pub fn getState(self: *const Self, comptime State: type) ?*State { if (self.initialized_states.get(.of(State))) |boxed_state| { return boxed_state.has(State).?; } @@ -65,22 +181,34 @@ pub fn hasState(self: *const Self, comptime State: type) ?*State { return null; } -pub fn init() error{OutOfMemory}!Self { +pub fn init() error{ OutOfMemory, SystemResources }!Self { var self = Self{ .initialized_states = .empty, .named_systems = .empty, }; - try self.setState(Time{ - .elapsed = 0, - }); - + try self.setState(Data{}); + try self.setState(Time{}); try ona.registerChannel(&self, Exit); + const data = self.getState(Data).?; + + try data.openNative(std.mem.span(c.SDL_GetBasePath())); + + if (builtin.mode == .Debug) { + const current_directory = c.SDL_GetCurrentDirectory(); + + defer { + c.SDL_free(current_directory); + } + + try data.openNative(std.mem.span(current_directory)); + } + return self; } -pub fn on(self: *Self, comptime schedule: anytype, behavior: *const Behavior) error{OutOfMemory}!void { +pub fn on(self: *Self, comptime schedule: anytype, behavior: *const ona.System) 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, .{})).?; diff --git a/src/ona/App/Setup.zig b/src/ona/Setup.zig similarity index 88% rename from src/ona/App/Setup.zig rename to src/ona/Setup.zig index d84fd8c..8186d92 100644 --- a/src/ona/App/Setup.zig +++ b/src/ona/Setup.zig @@ -1,6 +1,6 @@ const coral = @import("coral"); -const ona = @import("../ona.zig"); +const ona = @import("ona.zig"); const std = @import("std"); @@ -52,7 +52,17 @@ pub fn initModule(comptime namespace: anytype) Self { return init(namespace.setup); } -pub fn initSystem(comptime schedule: anytype, comptime behavior: *const ona.App.Behavior) Self { +pub fn initState(comptime state: anytype) Self { + const state_initialization = struct { + fn apply(app: *ona.App) !void { + try app.setState(state); + } + }; + + return init(state_initialization.apply); +} + +pub fn initSystem(comptime schedule: anytype, comptime behavior: *const ona.System) Self { const system_initialization = struct { fn apply(app: *ona.App) !void { try app.on(schedule, behavior); diff --git a/src/ona/App/Behavior.zig b/src/ona/System.zig similarity index 97% rename from src/ona/App/Behavior.zig rename to src/ona/System.zig index 7b45732..ee5abcb 100644 --- a/src/ona/App/Behavior.zig +++ b/src/ona/System.zig @@ -1,8 +1,8 @@ -const SystemGraph = @import("./SystemGraph.zig"); +const SystemGraph = @import("SystemGraph.zig"); const coral = @import("coral"); -const ona = @import("../ona.zig"); +const ona = @import("ona.zig"); const std = @import("std"); @@ -143,8 +143,6 @@ pub fn of(comptime function: anytype) *const Self { }; } - // TODO: Errdefer deinit the param states. - return .init(coral.heap.allocator, param_states); } @@ -182,9 +180,10 @@ pub fn of(comptime function: anytype) *const Self { }, .pointer => |pointer| { - init_args[arg_index] = app.hasState(pointer.child) orelse { - @panic(std.fmt.comptimePrint("{s} is a required state but not present in the App", .{ + init_args[arg_index] = app.getState(pointer.child) orelse { + @panic(std.fmt.comptimePrint("{s} is a required state but not present in the App. {s}", .{ @typeName(pointer.child), + "Did you mean to add it with App.putState?", })); }; }, diff --git a/src/ona/App/SystemGraph.zig b/src/ona/SystemGraph.zig similarity index 86% rename from src/ona/App/SystemGraph.zig rename to src/ona/SystemGraph.zig index f68afe1..b92935f 100644 --- a/src/ona/App/SystemGraph.zig +++ b/src/ona/SystemGraph.zig @@ -1,6 +1,6 @@ const coral = @import("coral"); -const ona = @import("../ona.zig"); +const ona = @import("ona.zig"); const std = @import("std"); @@ -15,7 +15,7 @@ run_arena: std.heap.ArenaAllocator = .init(coral.heap.allocator), const AccessMap = coral.tree.Binary(*const coral.TypeId, BehaviorSet, coral.tree.scalarTraits(*const coral.TypeId)); -const BehaviorSet = coral.stack.Sequential(*const ona.App.Behavior); +const BehaviorSet = coral.stack.Sequential(*const ona.System); const Edge = struct { dependencies: BehaviorSet = .empty, @@ -30,7 +30,7 @@ const Edge = struct { }; fn Map(comptime Payload: type) type { - return coral.tree.Binary(*const ona.App.Behavior, Payload, coral.tree.scalarTraits(*const ona.App.Behavior)); + return coral.tree.Binary(*const ona.System, Payload, coral.tree.scalarTraits(*const ona.System)); } const Processed = struct { @@ -38,7 +38,7 @@ const Processed = struct { }; const Work = struct { - behavior: *const ona.App.Behavior, + behavior: *const ona.System, local_state: coral.Box, }; @@ -55,6 +55,16 @@ pub const TypeDependency = struct { is_read_only: bool, }; +pub fn applyDeferred(self: *Self, app: *ona.App) void { + for (self.parallel_work.items.slice()) |work| { + work.behavior.apply(work.local_state, app); + } + + for (self.blocking_work.items.slice()) |work| { + work.behavior.apply(work.local_state, app); + } +} + pub fn deinit(self: *Self) void { const allocator = coral.heap.allocator; @@ -75,20 +85,20 @@ pub fn deinit(self: *Self) void { } } -pub fn dependOnBehavior(self: *Self, app: *ona.App, dependant: *const ona.App.Behavior, dependency: *const ona.App.Behavior) std.mem.Allocator.Error!void { +pub fn dependOnBehavior(self: *Self, app: *ona.App, dependant: *const ona.System, dependency: *const ona.System) std.mem.Allocator.Error!void { try self.insert(app, dependant); const edge = self.edges.get(dependant).?; - if (std.mem.indexOfScalar(*const ona.App.Behavior, edge.dependencies.items.slice(), dependency) == null) { + if (std.mem.indexOfScalar(*const ona.System, edge.dependencies.items.slice(), dependency) == null) { try edge.dependencies.pushGrow(coral.heap.allocator, dependency); } } -pub fn dependOnType(self: *Self, app: *ona.App, dependant: *const ona.App.Behavior, dependency: TypeDependency) std.mem.Allocator.Error!void { +pub fn dependOnType(self: *Self, app: *ona.App, dependant: *const ona.System, dependency: TypeDependency) std.mem.Allocator.Error!void { const readers = self.state_readers.get(dependency.id) orelse (try self.state_readers.insert(coral.heap.allocator, dependency.id, .empty)).?; - if (std.mem.indexOfScalar(*const ona.App.Behavior, readers.items.slice(), dependant)) |index| { + if (std.mem.indexOfScalar(*const ona.System, readers.items.slice(), dependant)) |index| { for (readers.items.slice()[0..index]) |reader| { try self.dependOnBehavior(app, dependant, reader); } @@ -107,7 +117,7 @@ pub fn dependOnType(self: *Self, app: *ona.App, dependant: *const ona.App.Behavi if (!dependency.is_read_only) { const writers = self.state_writers.get(dependency.id) orelse (try self.state_writers.insert(coral.heap.allocator, dependency.id, .empty)).?; - if (std.mem.indexOfScalar(*const ona.App.Behavior, writers.items.slice(), dependant)) |index| { + if (std.mem.indexOfScalar(*const ona.System, writers.items.slice(), dependant)) |index| { for (writers.items.slice()[0..index]) |reader| { try self.dependOnBehavior(app, dependant, reader); } @@ -125,17 +135,7 @@ pub fn dependOnType(self: *Self, app: *ona.App, dependant: *const ona.App.Behavi } } -pub fn applyDeferred(self: *Self, app: *ona.App) void { - for (self.parallel_work.items.slice()) |work| { - work.behavior.apply(work.local_state, app); - } - - for (self.blocking_work.items.slice()) |work| { - work.behavior.apply(work.local_state, app); - } -} - -pub fn insert(self: *Self, app: *ona.App, behavior: *const ona.App.Behavior) std.mem.Allocator.Error!void { +pub fn insert(self: *Self, app: *ona.App, behavior: *const ona.System) std.mem.Allocator.Error!void { self.processed.clear(); // TODO: Refactor so partial initialisation isn't happening here anymore. @@ -151,7 +151,7 @@ pub fn insert(self: *Self, app: *ona.App, behavior: *const ona.App.Behavior) std } } -fn process(self: *Self, behavior: *const ona.App.Behavior, edge: Edge) !Processed { +fn process(self: *Self, behavior: *const ona.System, edge: Edge) !Processed { var processed = Processed{ .is_blocking = behavior.is_blocking }; for (edge.dependencies.items.slice()) |dependency| { @@ -218,7 +218,7 @@ pub fn run(self: *Self, app: *const ona.App) RunError!void { var parallel_work = self.parallel_work.items.slice(); for (self.parallel_work_ranges.items.slice()) |parallel_work_range| { - _ = try coral.tasks.create(run_allocator, .high_priority, coral.CallTask(runWorkGroup){ + _ = try ona.tasks.create(run_allocator, .high_priority, ona.tasks.CallTask(runWorkGroup){ .args = .{ app, parallel_work[0..parallel_work_range] }, }); @@ -226,7 +226,7 @@ pub fn run(self: *Self, app: *const ona.App) RunError!void { } } - coral.tasks.finish(.high_priority); + ona.tasks.finish(.high_priority); runWorkGroup(app, self.blocking_work.items.slice()); } diff --git a/src/ona/asset.zig b/src/ona/asset.zig index d3f9f3f..3156f85 100644 --- a/src/ona/asset.zig +++ b/src/ona/asset.zig @@ -1,84 +1,291 @@ +const ona = @import("ona.zig"); + const coral = @import("coral"); const std = @import("std"); -pub const Handle = packed struct { - index: u24, - salt: u20, - type_id: u20, +pub const GenericHandle = struct { + type_id: *const coral.TypeId, + index: u32, + salt: u32, + + pub fn reify(self: GenericHandle, comptime Asset: type) ?Handle(Asset) { + if (self.type_id != coral.TypeId.of(Asset)) { + return null; + } + + return .{ + .index = self.index, + .salt = self.salt, + }; + } }; -pub const Path = struct { - buffer: [256]u8, -}; +pub fn Handle(comptime Asset: type) type { + return packed struct(u64) { + index: u32, + salt: u32, -pub fn Store(comptime Asset: type) type { + const Self = @This(); + + pub const asset_type_id = coral.TypeId.of(Asset); + + pub const default: Self = @bitCast(@as(u64, 0)); + + pub fn generic(self: Self) GenericHandle { + return .{ + .type_id = asset_type_id, + .index = self.index, + .salt = self.salt, + }; + } + }; +} + +pub fn ResolveQueue(comptime Asset: type) type { return struct { - released_handles: coral.stack.Sequential(Handle) = .empty, + arena: std.heap.ArenaAllocator = .init(coral.heap.allocator), - assets: coral.stack.Parallel(struct { - asset: Asset, - state: AssetState, + tasks: coral.stack.Sequential(struct { + future: *ona.tasks.Future(?Asset), + handle: AssetHandle, }) = .empty, - const AssetState = struct { - usage: enum { vacant, reserved, occupied }, - salt: u20, - }; + const AssetHandle = Handle(Asset); - pub fn reserve(self: *Store) std.mem.Allocator.Error!Handle { - if (self.released_handles.pop()) |handle| { - const state = &self.assets.items.slice(.state)[handle.index]; + fn ResolveTask(comptime Loadable: type) type { + const load_fn_name = "load"; - state.usage = .reserved; + const load_fn = coral.meta.hasFn(Loadable, load_fn_name) orelse { + @compileError(std.fmt.comptimePrint("{s} must contain a .{s} fn", .{ + @typeName(Loadable), + load_fn_name, + })); + }; - return handle; + const all_args: [2]type = .{ Loadable, ona.App.Data }; + const loadable_type_name = @typeName(Loadable); + + if (load_fn.params.len > all_args.len or !coral.meta.parametersMatch(load_fn.params, all_args[0..load_fn.params.len])) { + @compileError(std.fmt.comptimePrint("Fn {s}.{s} must accept only {s} or ({s}, {s}) to be a valid loadable type", .{ + loadable_type_name, + load_fn_name, + loadable_type_name, + loadable_type_name, + @typeName(ona.App.Data), + })); } - const handle = Handle{ - .type_id = type_id, - .index = self.assets.items.len, + const Result = coral.meta.UnwrappedError(load_fn.return_type.?); + + if (Result != Asset) { + const result_type_name = @typeName(Result); + + @compileError(std.fmt.comptimePrint("Fn {s}.{s} must return {s} or !{s}, not {s}", .{ + loadable_type_name, + load_fn_name, + result_type_name, + result_type_name, + @typeName(load_fn.return_type.?), + })); + } + + return struct { + loadable: Loadable, + future: ona.tasks.Future(?Result) = .unresolved, + app_data: ona.App.Data, + + pub fn run(self: *@This()) void { + const loaded = switch (load_fn.params.len) { + 1 => self.loadable.load(), + 2 => self.loadable.load(self.app_data), + else => unreachable, + }; + + std.debug.assert(self.future.resolve(loaded catch |load_error| nullify: { + std.log.err("Failed to resolve {s}: {s}", .{ + loadable_type_name, + @errorName(load_error), + }); + + break :nullify null; + })); + } + }; + } + + const Self = @This(); + + pub fn apply(self: *Self, app: *ona.App) void { + if (!self.tasks.isEmpty()) { + const store = app.getState(ResolvedStorage(Asset)).?; + + for (self.tasks.items.slice()) |tasks| { + std.debug.assert(store.resolve(tasks.handle, tasks.future.get())); + } + + self.tasks.clear(); + std.debug.assert(self.arena.reset(.free_all)); + } + } + + pub fn load(self: *Self, app_data: ona.App.Data, reserved_handle: AssetHandle, loadable: anytype) error{OutOfMemory}!void { + try self.tasks.pushGrow(coral.heap.allocator, undefined); + + errdefer { + std.debug.assert(self.tasks.pop() != null); + } + + const resolve = try ona.tasks.create(self.arena.allocator(), .low_priority, ResolveTask(@TypeOf(loadable)){ + .loadable = loadable, + .app_data = app_data, + }); + + std.debug.assert(self.tasks.set(.{ + .future = &resolve.future, + .handle = reserved_handle, + })); + } + + pub fn deinit(self: *Self) void { + std.debug.assert(self.tasks.isEmpty()); + self.tasks.deinit(coral.heap.allocator); + self.arena.deinit(); + } + }; +} + +pub fn ResolvedStorage(comptime Asset: type) type { + return struct { + free_index: u32 = 0, + + asset_entries: coral.stack.Parallel(struct { + value: Value, + entry: Entry, + + const Entry = struct { + usage: enum { vacant, reserved, occupied, missing }, + salt: u32, + }; + + const Value = union { + asset: Asset, + free_index: u32, + }; + }) = .empty, + + const AssetHandle = Handle(Asset); + + const Self = @This(); + + pub fn deinit(self: *Self) void { + // TODO: Cleanup assets. + // for (self.asset_entries.items.slice(.entry), 0 .. self.asset_entries.items.len) |entry, i| { + // if (entry != .occupied) { + // continue; + // } + + // self.asset_entries.items.slice(.value)[i].asset.deinit(); + // } + + self.asset_entries.deinit(coral.heap.allocator); + } + + pub fn get(self: *const Self, handle: AssetHandle) error{ExpiredHandle}!?Asset { + const entry = &self.asset_entries.items.slice(.entry)[handle.index]; + + if (handle.salt != entry.salt) { + return error.ExpiredHandle; + } + + const value = &self.asset_entries.items.slice(.value)[handle.index]; + + return switch (entry.usage) { + .vacant => error.ExpiredHandle, + .reserved, .missing => null, + .occupied => value.asset, + }; + } + + pub fn remove(self: *Self, handle: AssetHandle) ?Asset { + const entry = &self.asset_entries.items.slice(.entry)[handle.index]; + + if (handle.salt != entry.salt) { + return null; + } + + if (entry.usage != .occupied) { + return null; + } + + const value = &self.asset_entries.items.slice(.value)[handle.index]; + + defer { + entry.usage = .vacant; + entry.salt = @max(1, entry.salt +% 1); + value.* = .{ .free_index = self.free_index }; + self.free_index = value.free_index; + } + + return value.asset; + } + + pub fn reserve(self: *Self) error{OutOfMemory}!AssetHandle { + if (self.free_index < self.asset_entries.items.len) { + const entry = &self.asset_entries.items.slice(.entry)[self.free_index]; + + defer { + self.free_index = self.asset_entries.items.slice(.value)[self.free_index].free_index; + entry.usage = .reserved; + } + + return .{ + .index = self.free_index, + .salt = entry.salt, + }; + } + + const handle = AssetHandle{ + .index = self.asset_entries.items.len, .salt = 1, }; - try self.assets.pushGrow(coral.heap.allocator, .{ - .asset = undefined, + try self.asset_entries.pushGrow(coral.heap.allocator, .{ + .value = undefined, - .state = .{ + .entry = .{ .usage = .reserved, .salt = handle.salt, }, }); + self.free_index += 1; + return handle; } - pub fn resolve(self: *Store, reserved_handle: Handle, asset: Asset) bool { - const state = self.assets.items.slice(.state)[reserved_handle.index]; + pub fn resolve(self: *Self, reserved_handle: AssetHandle, resolved_asset: ?Asset) bool { + const index = reserved_handle.index; + const entry = &self.asset_entries.items.slice(.entry)[index]; - if (state.usage != .reserved) { + if (entry.usage != .reserved) { return false; } - if (reserved_handle.salt != state.salt) { + if (reserved_handle.salt != entry.salt) { return false; } - self.assets.items.slice(.asset)[reserved_handle.index] = asset; + if (resolved_asset) |asset| { + entry.usage = .occupied; + self.asset_entries.items.slice(.value)[index] = .{ .asset = asset }; + } else { + entry.usage = .missing; + } return true; } - - const type_id = generateTypeId(); }; } -fn generateTypeId() error{Overflow}!u20 { - const id = struct { - var last_assigned: u20 = 0; - }; - - id.last_assigned = try std.math.add(u20, id.last_assigned, 1); - - return id.last_assigned; -} +pub const extension_len_max = 7; diff --git a/src/ona/gfx.zig b/src/ona/gfx.zig index dcc46f4..0e0b824 100644 --- a/src/ona/gfx.zig +++ b/src/ona/gfx.zig @@ -1,79 +1,455 @@ -const Context = @import("./gfx/Context.zig"); +pub const Color = @import("gfx/Color.zig"); + +const builtin = @import("builtin"); const coral = @import("coral"); -const c = @cImport({ - @cInclude("SDL3/SDL.h"); - @cInclude("shaderc/shaderc.h"); -}); +const device = @import("gfx/device.zig"); -const glsl = @import("./gfx/glsl.zig"); +const glsl = @import("gfx/glsl.zig"); -const ona = @import("./ona.zig"); +const ona = @import("ona.zig"); + +const qoi = @import("gfx/qoi.zig"); const std = @import("std"); -pub const Commands = struct { - pending: *Context.Queue, +pub const CheckerImage = struct { + square_size: u32, + colors: [2]Color, + width: u32, + height: u32, - pub fn init(context: *const Context) !Commands { - return .{ - .pending = try context.shared.acquireQueue(), + pub fn load(self: CheckerImage) !device.Image { + var image = try device.Image.init(self.width, self.height, .rgba8, .{ .is_input = true }); + + errdefer { + image.deinit(); + } + + const image_size = image.height * image.width * device.Image.Format.rgba8.stride(); + + { + var image_memory = try device.Memory.init(image_size); + + defer { + image_memory.deinit(); + } + + if (self.square_size == 0) { + try coral.bytes.writeN(image_memory.writer(), &self.colors[0].toRgba8(), self.width * self.height); + } else { + for (0..self.width * self.height) |i| { + const x = i % self.width; + const y = i / self.width; + const square_x = x / self.square_size; + const square_y = y / self.square_size; + const color_index = (square_x + square_y) % 2; + + try coral.bytes.writeAll(image_memory.writer(), &self.colors[color_index].toRgba8()); + } + } + + image_memory.commit(); + + const commands = try device.acquireCommands(); + + commands.copyPass().uploadImage(image, image_memory).finish(); + commands.submit(); + } + + return image; + } +}; + +const Context = struct { + swapchain: device.Swapchain, + backbuffer: device.Target, + swap_buffers: [2]Buffer = .{ .{}, .{} }, + is_swapped: bool = false, + frame_arena: std.heap.ArenaAllocator = .init(coral.heap.allocator), + renderer: ?*RenderTask = null, + + const Buffer = struct { + head: ?*Queue = null, + tail: ?*Queue = null, + + fn dequeue(self: *Buffer) ?*Queue { + if (self.head) |head| { + self.head = head.next; + head.next = null; + + if (self.head == null) { + self.tail = null; + } + + return head; + } + + return null; + } + + fn enqueue(self: *Buffer, node: *Queue) void { + if (self.tail) |tail| { + std.debug.assert(self.head != null); + + tail.next = node; + self.tail = node; + } else { + self.head = node; + self.tail = node; + } + + node.next = null; + } + }; + + const RenderTask = struct { + backbuffer: device.Target, + submitted_buffer: *Buffer, + finished: std.Thread.ResetEvent = .{}, + + pub fn run(self: *RenderTask) void { + defer { + self.finished.set(); + } + + const commands = device.acquireCommands() catch |acquire_error| { + return std.log.err("Render task failed: {s}", .{@errorName(acquire_error)}); + }; + + var render_pass = commands.renderPass(self.backbuffer); + + while (self.submitted_buffer.dequeue()) |renderables| { + defer { + renderables.reset(); + } + + for (renderables.commands.items.slice()) |command| { + switch (command) {} + } + } + + render_pass.finish(); + commands.submit(); + } + }; + + pub fn deinit(self: *Context) void { + if (self.renderer) |renderer| { + renderer.finished.wait(); + } + + self.frame_arena.deinit(); + self.backbuffer.deinit(); + self.swapchain.deinit(); + device.stop(); + + self.* = undefined; + } + + pub fn finish(self: *Context) void { + if (self.renderer) |renderer| { + renderer.finished.wait(); + } + + if (!self.frame_arena.reset(.retain_capacity)) { + std.log.warn("Failed to retain capacity of {s} frame allocator, this may impact performance", .{ + @typeName(Context), + }); + } + + self.renderer = null; + self.is_swapped = !self.is_swapped; + + self.swapchain.present(self.backbuffer); + } + + pub fn render(self: *Context) void { + self.renderer = ona.tasks.create(self.frame_arena.allocator(), .multimedia, RenderTask{ + .submitted_buffer = &self.swap_buffers[@intFromBool(!self.is_swapped)], + .backbuffer = self.backbuffer, + }) catch |creation_error| { + return std.log.warn("{s}", .{switch (creation_error) { + error.OutOfMemory => "Not enough memory was avaialble to start a render, skipping frame...", + }}); }; } + + pub fn submit(self: *Context, renderables: *Queue) void { + self.swap_buffers[@intFromBool(self.is_swapped)].enqueue(renderables); + } }; pub const Display = struct { - width: u16, - height: u16, + size: [2]u16, is_hidden: bool, }; -pub fn synchronize(commands: ona.Commands) !void { - const buffer_swap = struct { - fn apply(app: *ona.App) void { - const context = app.hasState(Context).?; - const display = app.hasState(Display).?; +pub const Effects = ona.Assets(device.Effect); - context.shared.swapBuffers(display.*); +pub const Images = ona.Assets(device.Image); + +pub const QoiImage = struct { + path: ona.App.Path, + + pub fn load(self: QoiImage, data: ona.App.Data) !device.Image { + var qoi_data = coral.bytes.spanOf(try data.loadFile(self.path, coral.heap.allocator)); + + defer { + coral.heap.allocator.free(qoi_data.bytes); } + + const header = try qoi.Info.decode(qoi_data.reader()); + var image_memory = try device.Memory.init(header.width * header.height * device.Image.Format.rgba8.stride()); + + defer { + image_memory.deinit(); + } + + { + var decoder = qoi.DecodeStream.init(header); + + while (try decoder.fetch(qoi_data.reader())) |run| { + try coral.bytes.writeN(image_memory.writer(), &run.pixel.bytes(), run.count); + } + } + + image_memory.commit(); + + var image = try device.Image.init(header.width, header.height, .rgba8, .{ + .is_input = true, + }); + + errdefer { + image.deinit(); + } + + const commands = try device.acquireCommands(); + + commands.copyPass().uploadImage(image, image_memory).finish(); + commands.submit(); + + return image; + } +}; + +const Queue = struct { + commands: coral.stack.Sequential(Command) = .empty, + next: ?*Queue = null, + + const Command = union(enum) {}; + + pub fn apply(self: *Queue, app: *ona.App) void { + if (!self.commands.isEmpty()) { + const context = app.getState(Context).?; + + context.submit(self); + } + } + + pub fn reset(self: *Queue) void { + self.commands.clear(); + } +}; + +pub const Scene = struct { + renderables: *Queue, + image_store: *const Images.Store, + effect_store: *const Effects.Store, + + pub const Rect = struct { + left: f32, + top: f32, + right: f32, + bottom: f32, }; - try commands.push(buffer_swap.apply); + pub const SpriteDraw = struct { + anchor: [2]f32 = @splat(0), + size: ?@Vector(2, f32) = null, + source: Rect = .{ .left = 0, .top = 0, .right = 1, .bottom = 1 }, + position: [2]f32 = @splat(0), + tint: Color = .white, + rotation: f32 = 0, + depth: f32 = 0, + effect: Effects.Handle = .default, + }; + + pub fn bind() Queue { + return .{}; + } + + pub fn drawSprite(self: Scene, image: Images.Handle, draw: SpriteDraw) void { + _ = self; + _ = image; + _ = draw; + } + + pub fn updateEffect(self: Scene, effect: Effects.Handle, properties: anytype) void { + _ = self; + _ = effect; + _ = properties; + } + + pub fn updateImage(self: Scene, effect: Effects.Handle, properties: anytype) void { + _ = self; + _ = effect; + _ = properties; + } + + pub fn init(renderables: *Queue, image_store: *const Images.Store, effect_store: *const Effects.Store) Scene { + return .{ + .renderables = renderables, + .image_store = image_store, + .effect_store = effect_store, + }; + } +}; + +pub const GlslEffect = struct { + fragment: Source = .empty, + vertex: Source = .empty, + + pub const Source = glsl.Assembly.Source; + + pub fn load(self: GlslEffect) !device.Effect { + var arena = std.heap.ArenaAllocator.init(coral.heap.allocator); + + defer { + arena.deinit(); + } + + var assembly = try glsl.Assembly.init(.spirv, switch (builtin.mode) { + .ReleaseSafe, .Debug => .unoptimized, + .ReleaseFast => .optimize_speed, + .ReleaseSmall => .optimize_size, + }); + + defer { + assembly.deinit(); + } + + const Effect = extern struct { + time: f32, + }; + + const Camera = extern struct { + projection: [4]@Vector(4, f32), + }; + + const arena_allocator = arena.allocator(); + const effect_uniform_block = glsl.nativeUniformBlock(0, Effect); + const camera_uniform_block = glsl.nativeUniformBlock(1, Camera); + const default_shader_name = "[unnamed shader]"; + const glsl_version = 430; + + return device.Effect.init(.{ + .fragment_code = try assembly.assemble(arena_allocator, .fragment, .{ + .glsl = try glsl.inject(arena_allocator, switch (self.fragment.glsl.len) { + 0 => @embedFile("gfx/canvas.frag"), + else => self.fragment.glsl, + }, glsl_version, .{ + .vertex_uv = glsl.nativeInput(0, @Vector(2, f32)), + .vertex_rgba = glsl.nativeInput(1, @Vector(4, f32)), + .image = glsl.Sampler{ .sampler_2d = 0 }, + .color = glsl.nativeOutput(0, @Vector(4, f32)), + .effect = effect_uniform_block, + .camera = camera_uniform_block, + }), + + .name = switch (self.fragment.name.len) { + 0 => default_shader_name, + else => self.fragment.name, + }, + }), + + .vertex_code = try assembly.assemble(arena_allocator, .vertex, .{ + .glsl = try glsl.inject(arena_allocator, switch (self.vertex.glsl.len) { + 0 => @embedFile("gfx/canvas.vert"), + else => self.vertex.glsl, + }, glsl_version, .{ + .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)), + .effect = effect_uniform_block, + .camera = camera_uniform_block, + }), + + .name = switch (self.vertex.name.len) { + 0 => default_shader_name, + else => self.vertex.name, + }, + }), + }); + } +}; + +pub fn renderFrame(context: ona.Exclusive(Context), display: ona.Read(Display)) void { + if (display.ptr.is_hidden) { + _ = context.ptr.swapchain.hide(); + } else { + _ = context.ptr.swapchain.show(); + } + + context.ptr.render(); } pub fn setup(app: *ona.App) !void { - if (!c.SDL_Init(c.SDL_INIT_VIDEO)) { - return error.SystemFailure; + try device.start(); + + errdefer { + device.stop(); } - const default_display = Display{ - .width = 1280, - .height = 720, - .is_hidden = false, - }; + var arena = std.heap.ArenaAllocator.init(coral.heap.allocator); - try app.setState(default_display); + defer { + arena.deinit(); + } - // var shader_compiler = try glsl.Assembly.init(.spirv, switch (builtin.mode) { - // .Debug, .ReleaseSafe => .unoptimized, - // .ReleaseSmall => .optimize_size, - // .ReleaseFast => .optimize_speed, - // }); - - // errdefer { - // shader_compiler.deinit(); - // } + const default_width, const default_height = .{ 1280, 720 }; { - var context = try Context.init(default_display); + var swapchain = try device.Swapchain.init(default_width, default_height, "Ona"); errdefer { - context.deinit(); + swapchain.deinit(); } - try app.setState(context); + var backbuffer = try device.Target.init(default_width, default_height); + + errdefer { + backbuffer.deinit(); + } + + try app.setState(Context{ + .swapchain = swapchain, + .backbuffer = backbuffer, + }); } - try app.on(.render, .of(synchronize)); + try app.setState(Display{ + .size = .{ default_width, default_height }, + .is_hidden = false, + }); + + try ona.registerAsset(app, device.Image); + try ona.registerAsset(app, device.Effect); + try app.on(.pre_update, .of(renderFrame)); + try app.on(.post_update, .of(swapBuffers)); +} + +pub fn swapBuffers(context: ona.Exclusive(Context)) void { + context.ptr.finish(); } diff --git a/src/ona/gfx/Color.zig b/src/ona/gfx/Color.zig new file mode 100644 index 0000000..ac153e0 --- /dev/null +++ b/src/ona/gfx/Color.zig @@ -0,0 +1,34 @@ +const std = @import("std"); + +r: f32, +g: f32, +b: f32, +a: f32 = 1, + +const Self = @This(); + +pub const Vector = @Vector(4, f32); + +pub fn toRgba8(self: Self) [4]u8 { + const lower_limit = 0; + const upper_limit = std.math.maxInt(u8); + + return .{ + @intFromFloat(std.math.clamp(self.r * upper_limit, lower_limit, upper_limit)), + @intFromFloat(std.math.clamp(self.g * upper_limit, lower_limit, upper_limit)), + @intFromFloat(std.math.clamp(self.b * upper_limit, lower_limit, upper_limit)), + @intFromFloat(std.math.clamp(self.a * upper_limit, lower_limit, upper_limit)), + }; +} + +pub fn vector(self: Self) Vector { + return .{ self.r, self.g, self.b, self.a }; +} + +pub const black = Self{ .r = 0, .g = 0, .b = 0 }; + +pub const grey = Self{ .r = 0.5, .g = 0.5, .b = 0.5 }; + +pub const purple = Self{ .r = 0.5, .g = 0, .b = 0.5 }; + +pub const white = Self{ .r = 1, .g = 1, .b = 1 }; diff --git a/src/ona/gfx/Context.zig b/src/ona/gfx/Context.zig deleted file mode 100644 index a1e3637..0000000 --- a/src/ona/gfx/Context.zig +++ /dev/null @@ -1,250 +0,0 @@ -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 reset(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.reset(); - - 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/canvas.vert b/src/ona/gfx/canvas.vert index a59d21b..edaf72b 100644 --- a/src/ona/gfx/canvas.vert +++ b/src/ona/gfx/canvas.vert @@ -2,11 +2,11 @@ void main() { const vec2 local_xy = model_xy + instance_pivot; const vec2 world_position = instance_origin + (local_xy.x * instance_xbasis) + (local_xy.y * instance_ybasis); - const vec2 projected = (camera.projection * vec4(world_position, 0, 1)).xy; + const vec2 projected = (camera.projection * vec4(world_position, 0, 1)).xy; const vec2 depth_alpha = unpackHalf2x16(instance_bits); - gl_Position = vec4(projected, depth_alpha.x, 1.0); + gl_Position = vec4(projected, depth_alpha.x, 1.0); - rgba = model_rgba * vec4(instance_rgb, depth_alpha.y); - uv = instance_uv_offset + (model_uv * instance_uv_scale); + rgba = model_rgba * vec4(instance_rgb, depth_alpha.y); + uv = instance_uv_offset + (model_uv * instance_uv_scale); } diff --git a/src/ona/gfx/composite.frag b/src/ona/gfx/composite.frag deleted file mode 100644 index 3131347..0000000 --- a/src/ona/gfx/composite.frag +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index c626c5a..0000000 --- a/src/ona/gfx/composite.vert +++ /dev/null @@ -1,25 +0,0 @@ - -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/device.zig b/src/ona/gfx/device.zig new file mode 100644 index 0000000..aecfa6f --- /dev/null +++ b/src/ona/gfx/device.zig @@ -0,0 +1,435 @@ +const Color = @import("Color.zig"); + +const builtin = @import("builtin"); + +const c = @cImport({ + @cInclude("SDL3/SDL.h"); +}); + +const coral = @import("coral"); + +const std = @import("std"); + +pub const Commands = opaque { + pub const CopyPass = opaque { + pub fn finish(self: *CopyPass) void { + c.SDL_EndGPUCopyPass(@ptrCast(self)); + } + + pub fn uploadImage(self: *CopyPass, image: Image, memory: Memory) *CopyPass { + c.SDL_UploadToGPUTexture(@ptrCast(self), &.{ .transfer_buffer = memory.buffer }, &.{ + .texture = image.texture, + .w = image.width, + .h = image.height, + .d = 1, + }, false); + + return self; + } + }; + + pub const RenderPass = struct { + pub fn finish(self: *RenderPass) void { + c.SDL_EndGPURenderPass(@ptrCast(self)); + } + }; + + pub fn copyPass(self: *Commands) *CopyPass { + // Beginning a copy pass cannot fail unless the command buffer is invalid. + return @ptrCast(c.SDL_BeginGPUCopyPass(@ptrCast(self))); + } + + pub fn cancel(self: *Commands) void { + _ = c.SDL_CancelGPUCommandBuffer(@ptrCast(self)); + } + + pub fn renderPass(self: *Commands, target: Target) *RenderPass { + const color_targets: []const c.SDL_GPUColorTargetInfo = &.{ + .{ + .texture = target.color, + .load_op = c.SDL_GPU_LOADOP_CLEAR, + .clear_color = @bitCast(Color.black.vector()), + }, + }; + + // Beginning a render pass cannot fail unless the command buffer is invalid. + return @ptrCast(c.SDL_BeginGPURenderPass(@ptrCast(self), color_targets.ptr, @intCast(color_targets.len), &.{ + .texture = target.depth_stencil, + .load_op = c.SDL_GPU_LOADOP_CLEAR, + .clear_depth = 0, + })); + } + + pub fn submit(self: *Commands) void { + _ = c.SDL_SubmitGPUCommandBuffer(@ptrCast(self)); + } +}; + +pub const Effect = struct { + pipeline: *c.SDL_GPUGraphicsPipeline, + + pub const Spirv = struct { + fragment_code: []const u8, + fragment_entrypoint: [:0]const u8 = "main", + vertex_code: []const u8 = &.{}, + vertex_entrypoint: [:0]const u8 = "main", + }; + + var default_vertex_shader: *c.SDL_GPUShader = undefined; + + pub fn deinit(self: *Effect) void { + c.SDL_ReleaseGPUGraphicsPipeline(device, self.pipeline); + + self.* = undefined; + } + + pub fn init(spirv: Spirv) error{SystemResources}!Effect { + const fragment_shader = c.SDL_CreateGPUShader(device, &.{ + .code = spirv.fragment_code.ptr, + .code_size = spirv.fragment_code.len, + .format = c.SDL_GPU_SHADERFORMAT_SPIRV, + .stage = c.SDL_GPU_SHADERSTAGE_FRAGMENT, + .entrypoint = spirv.fragment_entrypoint, + }) orelse { + return error.SystemResources; + }; + + defer { + c.SDL_ReleaseGPUShader(device, fragment_shader); + } + + const vertex_shader = c.SDL_CreateGPUShader(device, &.{ + .code = spirv.vertex_code.ptr, + .code_size = spirv.vertex_code.len, + .format = c.SDL_GPU_SHADERFORMAT_SPIRV, + .stage = c.SDL_GPU_SHADERSTAGE_VERTEX, + .entrypoint = spirv.vertex_entrypoint, + }) orelse { + return error.SystemResources; + }; + + defer { + c.SDL_ReleaseGPUShader(device, vertex_shader); + } + + const pipeline = c.SDL_CreateGPUGraphicsPipeline(device, &.{ + .fragment_shader = fragment_shader, + .vertex_shader = vertex_shader, + .primitive_type = c.SDL_GPU_PRIMITIVETYPE_TRIANGLELIST, + + .depth_stencil_state = .{ + .enable_depth_write = true, + .compare_op = c.SDL_GPU_COMPAREOP_GREATER, + }, + }) orelse { + return error.SystemResources; + }; + + errdefer { + c.SDL_ReleaseGPUGraphicsPipeline(device, pipeline); + } + + return .{ + .pipeline = pipeline, + }; + } +}; + +pub const Image = struct { + texture: *c.SDL_GPUTexture, + width: u32, + height: u32, + format: Format, + attributes: Attributes, + + pub const Attributes = packed struct { + is_input: bool = false, + is_output: bool = false, + }; + + pub const Format = enum { + rgba8, + + pub fn stride(self: Format) usize { + return switch (self) { + .rgba8 => @sizeOf([4]u8), + }; + } + }; + + pub fn deinit(self: *Image) void { + c.SDL_ReleaseGPUTexture(device, self.texture); + + self.* = undefined; + } + + pub fn init(width: usize, height: usize, format: Format, attributes: Attributes) error{SystemResources}!Image { + if (width == 0 or height == 0) { + return error.SystemResources; + } + + const constrained_width = std.math.cast(u32, width) orelse { + return error.SystemResources; + }; + + const constrained_height = std.math.cast(u32, height) orelse { + return error.SystemResources; + }; + + var usage: c.SDL_GPUTextureUsageFlags = 0; + + if (attributes.is_input) { + usage |= c.SDL_GPU_TEXTUREUSAGE_SAMPLER; + } + + return .{ + .width = constrained_width, + .height = constrained_height, + .format = format, + .attributes = attributes, + + .texture = c.SDL_CreateGPUTexture(device, &.{ + .type = c.SDL_GPU_TEXTURETYPE_2D, + .width = constrained_width, + .height = constrained_height, + .layer_count_or_depth = 1, + .num_levels = 1, + .usage = usage, + + .format = switch (format) { + .rgba8 => c.SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM, + }, + }) orelse { + return error.SystemResources; + }, + }; + } +}; + +pub const Memory = struct { + buffer: *c.SDL_GPUTransferBuffer, + mapped: ?[*]u8 = null, + unused: usize, + + pub fn commit(self: *Memory) void { + c.SDL_UnmapGPUTransferBuffer(device, self.buffer); + + self.mapped = null; + } + + pub fn deinit(self: *Memory) void { + c.SDL_ReleaseGPUTransferBuffer(device, self.buffer); + } + + pub fn init(byte_size: usize) error{SystemResources}!Memory { + const buffer = c.SDL_CreateGPUTransferBuffer(device, &.{ + .size = std.math.cast(u32, byte_size) orelse { + return error.SystemResources; + }, + + .usage = c.SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD, + }) orelse { + return error.SystemResources; + }; + + errdefer { + c.SDL_ReleaseGPUTransferBuffer(device, buffer); + } + + const mapped = c.SDL_MapGPUTransferBuffer(device, buffer, false) orelse { + return error.SystemResources; + }; + + return .{ + .mapped = @ptrCast(mapped), + .buffer = buffer, + .unused = byte_size, + }; + } + + pub fn isCommited(self: Memory) bool { + return self.mapped == null; + } + + pub fn isFilled(self: Memory) bool { + return self.unused == 0; + } + + pub fn write(memory: *Memory, data: []const u8) usize { + const len = @min(data.len, memory.unused); + + @memcpy(memory.mapped.?[0..len], data[0..len]); + + memory.unused -= len; + + return len; + } + + pub fn writer(self: *Memory) coral.bytes.Writable { + return .initRef(self, write); + } +}; + +pub const Swapchain = struct { + window: *c.SDL_Window, + + pub fn hide(self: Swapchain) bool { + const is_hidden = (c.SDL_GetWindowFlags(self.window) & c.SDL_WINDOW_HIDDEN) != 0; + + if (is_hidden) { + return true; + } + + return c.SDL_HideWindow(self.window); + } + + pub fn deinit(self: *Swapchain) void { + c.SDL_DestroyWindow(self.window); + + self.* = undefined; + } + + pub fn init(width: u32, height: u32, title: [:0]const u8) error{SystemResources}!Swapchain { + // TODO: Unsafe int casts. + const window = c.SDL_CreateWindow(title, @intCast(width), @intCast(height), c.SDL_WINDOW_HIDDEN) orelse { + return error.SystemResources; + }; + + errdefer { + c.SDL_DestroyWindow(window); + } + + if (!c.SDL_ClaimWindowForGPUDevice(device, window)) { + return error.SystemResources; + } + + return .{ + .window = window, + }; + } + + pub fn present(self: Swapchain, target: Target) void { + if (c.SDL_AcquireGPUCommandBuffer(device)) |commands| { + defer { + _ = c.SDL_SubmitGPUCommandBuffer(commands); + } + + var swapchain_texture: ?*c.SDL_GPUTexture = null; + var sawpchain_width, var swapchain_height = [2]u32{ 0, 0 }; + + if (c.SDL_WaitAndAcquireGPUSwapchainTexture( + commands, + self.window, + &swapchain_texture, + &sawpchain_width, + &swapchain_height, + )) { + _ = c.SDL_BlitGPUTexture(commands, &.{ + .load_op = c.SDL_GPU_LOADOP_DONT_CARE, + + .source = .{ + .texture = target.color, + .w = sawpchain_width, + .h = swapchain_height, + }, + + .destination = .{ + .texture = swapchain_texture, + .w = sawpchain_width, + .h = swapchain_height, + }, + }); + } + } + } + + pub fn show(self: Swapchain) bool { + const is_shown = (c.SDL_GetWindowFlags(self.window) & c.SDL_WINDOW_HIDDEN) == 0; + + if (is_shown) { + return true; + } + + return c.SDL_ShowWindow(self.window); + } +}; + +pub const Target = struct { + color: *c.SDL_GPUTexture, + depth_stencil: *c.SDL_GPUTexture, + width: u16, + height: u16, + + pub fn deinit(self: *Target) void { + c.SDL_ReleaseGPUTexture(device, self.color); + c.SDL_ReleaseGPUTexture(device, self.depth_stencil); + + self.* = undefined; + } + + pub fn init(width: u16, height: u16) error{SystemResources}!Target { + const color = c.SDL_CreateGPUTexture(device, &.{ + .type = c.SDL_GPU_TEXTURETYPE_2D, + .width = width, + .height = height, + .layer_count_or_depth = 1, + .num_levels = 1, + .format = c.SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM, + .usage = c.SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | c.SDL_GPU_TEXTUREUSAGE_SAMPLER, + }) orelse { + return error.SystemResources; + }; + + errdefer { + c.SDL_ReleaseGPUTexture(device, color); + } + + const depth_stencil = c.SDL_CreateGPUTexture(device, &.{ + .type = c.SDL_GPU_TEXTURETYPE_2D, + .width = width, + .height = height, + .layer_count_or_depth = 1, + .num_levels = 1, + .format = c.SDL_GPU_TEXTUREFORMAT_D32_FLOAT, + .usage = c.SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET, + }) orelse { + return error.SystemResources; + }; + + errdefer { + c.SDL_ReleaseGPUTexture(device, depth_stencil); + } + + return .{ + .color = color, + .depth_stencil = depth_stencil, + .width = width, + .height = height, + }; + } +}; + +var device: *c.SDL_GPUDevice = undefined; + +pub fn acquireCommands() error{SystemResources}!*Commands { + return @ptrCast(c.SDL_AcquireGPUCommandBuffer(device) orelse { + return error.SystemResources; + }); +} + +pub fn start() error{SystemResources}!void { + if (!c.SDL_InitSubSystem(c.SDL_INIT_VIDEO)) { + return error.SystemResources; + } + + device = c.SDL_CreateGPUDevice(c.SDL_GPU_SHADERFORMAT_SPIRV, builtin.mode == .Debug, null) orelse { + return error.SystemResources; + }; +} + +pub fn stop() void { + c.SDL_DestroyGPUDevice(device); + c.SDL_QuitSubSystem(c.SDL_INIT_VIDEO); + + device = undefined; +} diff --git a/src/ona/gfx/glsl.zig b/src/ona/gfx/glsl.zig index 8f4fcea..4123456 100644 --- a/src/ona/gfx/glsl.zig +++ b/src/ona/gfx/glsl.zig @@ -4,8 +4,6 @@ const c = @cImport({ const coral = @import("coral"); -const ona = @import("../ona.zig"); - const std = @import("std"); /// @@ -24,6 +22,27 @@ pub const Assembly = struct { optimize_size, }; + /// + /// Textual GLSL source code with an associated name. + /// + pub const Source = struct { + name: [:0]const u8, + glsl: []const u8, + + pub const empty = Source{ + .name = "", + .glsl = "", + }; + }; + + /// + /// Different types of shader execution stages. + /// + pub const Stage = enum { + vertex, + fragment, + }; + /// /// Target format used for shader binaries. /// @@ -50,13 +69,12 @@ pub const Assembly = struct { 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) { + source: Source, + ) (std.mem.Allocator.Error || error{BadSyntax})![]u8 { + const result = c.shaderc_compile_into_spv(self.compiler, source.glsl.ptr, source.glsl.len, switch (stage) { .fragment => c.shaderc_glsl_fragment_shader, .vertex => c.shaderc_glsl_vertex_shader, - }, name, "main", self.spirv_options) orelse { + }, source.name.ptr, "main", self.options) orelse { return error.OutOfMemory; }; @@ -74,7 +92,7 @@ pub const Assembly = struct { 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}); + std.log.debug("problematic shader:\n{s}", .{source.glsl}); return error.BadSyntax; }, @@ -85,8 +103,8 @@ pub const Assembly = struct { } pub fn deinit(self: *Assembly) void { - c.shaderc_compile_options_release(self.spirv_options); - c.shaderc_compiler_release(self.shader_compiler); + c.shaderc_compile_options_release(self.options); + c.shaderc_compiler_release(self.compiler); self.* = undefined; } @@ -212,11 +230,6 @@ pub const Sampler = union(enum) { } }; -pub const Stage = enum { - vertex, - fragment, -}; - pub const UniformBlock = struct { name: [:0]const u8, binding: usize, @@ -321,7 +334,7 @@ pub fn inject(allocator: std.mem.Allocator, source_base: []const u8, version: us try field_value.serialize(field.name, output); } - try coral.bytes.writeAll(output, "#line 1\n"); + try coral.bytes.writeAll(output, "#line 0\n"); } }; diff --git a/src/ona/gfx/qoi.zig b/src/ona/gfx/qoi.zig new file mode 100644 index 0000000..14fdc36 --- /dev/null +++ b/src/ona/gfx/qoi.zig @@ -0,0 +1,177 @@ +const coral = @import("coral"); + +const std = @import("std"); + +pub const DecodeStream = struct { + last_pixel: Pixel, + pixel_index: [hash_max]Pixel, + remaining: usize, + + const Hash = u6; + + pub const Run = struct { + pixel: Pixel, + count: u32, + }; + + pub fn fetch(self: *DecodeStream, reader: coral.bytes.Readable) DecodeError!?Run { + if (self.remaining == 0) { + const terminator = &.{ 0, 0, 0, 0, 0, 0, 0, 1 }; + const message = try coral.bytes.readLittle(reader, [8]u8); + + if (!std.mem.eql(u8, &message, terminator)) { + return error.BadFormat; + } + + return null; + } + + const chunk = try coral.bytes.readBig(reader, enum(u8) { + rgb = 0xfe, + rgba = 0xff, + _, + }); + + var run_count: u32 = 1; + + switch (chunk) { + .rgb => { + self.last_pixel.r, self.last_pixel.g, self.last_pixel.b = try coral.bytes.readBig(reader, [3]u8); + }, + + .rgba => { + self.last_pixel.r, self.last_pixel.g, self.last_pixel.b, self.last_pixel.a = try coral.bytes.readBig(reader, [4]u8); + }, + + _ => { + const byte = @intFromEnum(chunk); + const op = byte & 0xc0; + const arg: u6 = @intCast(byte & 0x3f); + + switch (op) { + 0x00 => { + self.last_pixel = self.pixel_index[arg]; + }, + + 0x40 => { + const diff: packed struct(u6) { r: i2, g: i2, b: i2 } = @bitCast(arg); + + self.last_pixel = self.last_pixel.diff(diff.r, diff.g, diff.b); + }, + + 0x80 => { + const g_diff = @as(i6, @intCast(@as(i8, @as(u6, @truncate(arg))) - 32)); + const luma: packed struct(u8) { r_minus_g_diff: i4, b_minus_g_diff: i4 } = @bitCast(try coral.bytes.readByte(reader)); + const r_diff = @as(i8, g_diff) + luma.r_minus_g_diff; + const b_diff = @as(i8, g_diff) + luma.b_minus_g_diff; + + self.last_pixel = self.last_pixel.diff(r_diff, g_diff, b_diff); + }, + + 0xc0 => { + run_count += arg; + }, + + else => { + return error.BadFormat; + }, + } + }, + } + + self.pixel_index[hash(self.last_pixel)] = self.last_pixel; + self.remaining -= run_count; + + return .{ + .pixel = self.last_pixel, + .count = run_count, + }; + } + + fn hash(pixel: Pixel) Hash { + return @intCast(((pixel.r *% 3) +% (pixel.g *% 5) +% (pixel.b *% 7) +% (pixel.a *% 11)) % hash_max); + } + + const hash_max = std.math.maxInt(Hash) + 1; + + pub fn init(info: Info) DecodeStream { + return .{ + .pixel_index = std.mem.zeroes([hash_max]Pixel), + .last_pixel = .{ .r = 0, .g = 0, .b = 0, .a = std.math.maxInt(u8) }, + .remaining = info.width * info.height, + }; + } +}; + +pub const DecodeError = coral.bytes.ReadWriteError || error{ + BadFormat, +}; + +pub const Info = struct { + width: u32, + height: u32, + channels: Channels, + colorspace: Colorspace, + + pub const Channels = enum { rgb, rgba }; + + pub const Colorspace = enum { srgb, linear }; + + pub fn decode(reader: coral.bytes.Readable) DecodeError!Info { + const magic = try coral.bytes.readLittle(reader, [4]u8); + + if (!std.mem.eql(u8, &magic, "qoif")) { + return error.BadFormat; + } + + const width, const height = try coral.bytes.readBig(reader, [2]u32); + + const format = try coral.bytes.readLittle(reader, extern struct { + channels: u8, + colorspace: u8, + }); + + return .{ + .channels = switch (format.channels) { + 3 => .rgb, + 4 => .rgba, + + else => { + return error.BadFormat; + }, + }, + + .colorspace = switch (format.colorspace) { + 0 => .srgb, + 1 => .linear, + + else => { + return error.BadFormat; + }, + }, + + .width = width, + .height = height, + }; + } +}; + +pub const Pixel = struct { + r: u8, + g: u8, + b: u8, + a: u8, + + pub fn bytes(self: Pixel) [4]u8 { + return .{ self.r, self.g, self.b, self.a }; + } + + pub fn diff(self: Pixel, r_diff: i8, g_diff: i8, b_diff: i8) Pixel { + return .{ + .r = self.r +% @as(u8, @bitCast(r_diff)), + .g = self.g +% @as(u8, @bitCast(g_diff)), + .b = self.b +% @as(u8, @bitCast(b_diff)), + .a = self.a, + }; + } +}; diff --git a/src/ona/ona.zig b/src/ona/ona.zig index eb8febf..dc2bc9d 100644 --- a/src/ona/ona.zig +++ b/src/ona/ona.zig @@ -1,47 +1,62 @@ -pub const App = @import("./App.zig"); +pub const App = @import("App.zig"); -const asset = @import("./asset.zig"); +pub const Setup = @import("Setup.zig"); + +pub const System = @import("System.zig"); + +const asset = @import("asset.zig"); const coral = @import("coral"); -pub const gfx = @import("./gfx.zig"); +pub const gfx = @import("gfx.zig"); -pub const hid = @import("./hid.zig"); +pub const hid = @import("hid.zig"); const std = @import("std"); -pub const AssetHandle = asset.Handle; - -pub const AssetPath = asset.Path; +pub const tasks = @import("tasks.zig"); pub fn Assets(comptime Asset: type) type { - const Store = asset.Store(Asset); - return struct { - cached: *Store, + app_data: App.Data, + resolved: *Store, + resolves: *ResolveQueue, + + pub const Handle = asset.Handle(Asset); + + const ResolveQueue = asset.ResolveQueue(Asset); const Self = @This(); - pub fn init(store: *Store) Self { + pub const Store = asset.ResolvedStorage(Asset); + + pub fn bind() ResolveQueue { + return .{}; + } + + pub fn init(resolves: *ResolveQueue, resolved: *Store, app_data: *const App.Data) Self { return .{ - .store = store, + .resolved = resolved, + .resolves = resolves, + .app_data = app_data.*, }; } - pub fn insert(self: Self, uncached_asset: Asset) std.mem.Allocator.Error!AssetHandle { - const reserved_handle = try self.store.reserve(); + pub fn load(self: Self, loadable: anytype) error{OutOfMemory}!Handle { + const reserved_handle = try self.resolved.reserve(); - std.debug.assert(self.store.resolve(reserved_handle, uncached_asset)); + errdefer { + std.debug.assert(self.resolved.resolve(reserved_handle, null)); + } + + try self.resolves.load(self.app_data, reserved_handle, loadable); return reserved_handle; } - pub fn load(self: Self, load_path: AssetPath) std.mem.Allocator.Error!AssetHandle { - const reserved_handle = try self.store.reserve(); - - _ = load_path; - - return reserved_handle; + pub fn unload(self: Self, handle: Handle) void { + // TODO: Implement. + _ = self.resolved.remove(handle); } }; } @@ -230,33 +245,20 @@ pub fn Write(comptime State: type) type { }; } -pub const realtime_app = App.Setup.init(run_realtime_loop); +pub const realtime_app = Setup.init(realtimeLoop); -pub fn registerAsset(app: *App, comptime Asset: type) std.mem.Allocator.Error!void { - const AssetStore = asset.Store(Asset); - - try app.setState(AssetStore{}); -} - -pub fn registerChannel(app: *App, comptime Message: type) std.mem.Allocator.Error!void { - const MessageChannel = Channel(Message); - - try app.on(.post_update, .of(MessageChannel.swap)); - try app.setState(MessageChannel{}); -} - -fn run_realtime_loop(app: *App) !void { - // ext.SDL_SetLogPriorities(ext.SDL_LOG_PRIORITY_VERBOSE); - // ext.SDL_SetLogOutputFunction(sdl_log, null); +fn realtimeLoop(app: *App) !void { + // c.SDL_SetLogPriorities(c.SDL_LOG_PRIORITY_VERBOSE); + // c.SDL_SetLogOutputFunction(sdl_log, null); const updates_per_frame = 60.0; const target_frame_time = 1.0 / updates_per_frame; - const time = app.hasState(App.Time).?; - const exit_channel = app.hasState(Channel(App.Exit)).?; + const time = app.getState(App.Time).?; + const exit_channel = app.getState(Channel(App.Exit)).?; - try coral.tasks.start(); + try tasks.start(); defer { - coral.tasks.stop(); + tasks.stop(); } try app.run(.load); @@ -280,7 +282,6 @@ fn run_realtime_loop(app: *App) !void { } try app.run(.post_update); - try app.run(.render); const exit_messages = exit_channel.messages(); @@ -299,3 +300,15 @@ fn run_realtime_loop(app: *App) !void { } } } + +pub fn registerAsset(app: *App, comptime Asset: type) error{OutOfMemory}!void { + try app.setState(asset.ResolvedStorage(Asset){}); + try app.setState(asset.ResolveQueue(Asset){}); +} + +pub fn registerChannel(app: *App, comptime Message: type) error{OutOfMemory}!void { + const MessageChannel = Channel(Message); + + try app.on(.post_update, .of(MessageChannel.swap)); + try app.setState(MessageChannel{}); +} diff --git a/src/ona/tasks.zig b/src/ona/tasks.zig new file mode 100644 index 0000000..4bf314e --- /dev/null +++ b/src/ona/tasks.zig @@ -0,0 +1,192 @@ +const Queue = @import("./tasks/Queue.zig"); + +const builtin = @import("builtin"); + +const coral = @import("coral"); + +const std = @import("std"); + +pub fn CallTask(comptime function: anytype) type { + const Function = @TypeOf(function); + + const function_fn = switch (@typeInfo(Function)) { + .@"fn" => |@"fn"| @"fn", + else => @compileError("expeceted param `function` to be an fn type"), + }; + + return struct { + args: std.meta.ArgsTuple(Function), + future: Future(Return) = .unresolved, + + const Return = function_fn.return_type.?; + + const Self = @This(); + + pub fn run(self: *Self) void { + std.debug.assert(self.future.resolve(@call(.auto, function, self.args))); + } + }; +} + +pub const Category = enum { + high_priority, + low_priority, + multimedia, + + fn getThreadCount(self: Category, cpu_count: usize) usize { + const cpu_share: f64 = @floatFromInt(cpu_count); + + return switch (self) { + .high_priority => @intFromFloat(cpu_share * 0.375), + .low_priority => @intFromFloat(cpu_share * 0.25), + .multimedia => 1, + }; + } +}; + +pub fn Future(comptime Payload: type) type { + return struct { + payload: Payload, + resolved: std.Thread.ResetEvent, + + const Self = @This(); + + pub const unresolved = Self{ + .payload = undefined, + .resolved = .{}, + }; + + pub fn get(self: *Self) Payload { + self.resolved.wait(); + + return self.payload; + } + + pub fn resolve(self: *Self, payload: Payload) bool { + if (self.resolved.isSet()) { + return false; + } + + self.payload = payload; + + self.resolved.set(); + + return true; + } + }; +} + +const categories = std.enums.values(Category); + +var categorised_tasks: [categories.len]?*Queue = [_]?*Queue{null} ** categories.len; + +/// +/// Allocates and pushes a copy of `task` onto the task queue for `category`, blocking the current thread if the queue +/// is currently full. +/// +/// Any container type supplying a `run` function declaration matching `fn (*TypeOf(task)) void` may be used as a "task +/// type". +/// +/// A reference to the allocated memory is returned or `std.mem.Allocator.Error`. +/// +/// **Note** any allocated memory successfully returned must be freed at a later point, either directly or indirectly. +/// +pub fn create(allocator: std.mem.Allocator, category: Category, task: anytype) std.mem.Allocator.Error!*@TypeOf(task) { + const Task = @TypeOf(task); + + const run_fn = coral.meta.hasFn(Task, "run") orelse { + @compileError(std.fmt.comptimePrint("{s} requires a .run fn to be a valid task type", .{@typeName(Task)})); + }; + + if (run_fn.return_type.? != void) { + @compileError(std.fmt.comptimePrint("{s}.run fn must return a void type", .{@typeName(Task)})); + } + + if (run_fn.params.len != 1 or run_fn.params[0].type != *Task) { + @compileError(std.fmt.comptimePrint("{s}.run fn must accept a {s} as it's one and only parameter, not {s}", .{ + @typeName(Task), + @typeName(*Task), + if (run_fn.params[0].type) |Type| @typeName(Type) else "anytype", + })); + } + + const created_task = try allocator.create(Task); + + created_task.* = task; + + if (categorised_tasks[@intFromEnum(category)]) |tasks| { + tasks.enqueue(.initRef(created_task, Task.run)); + } else { + created_task.run(); + } + + return created_task; +} + +/// +/// Blocks the calling thread to wait until all tasks under `category` are finished. +/// +pub fn finish(category: Category) void { + if (categorised_tasks[@intFromEnum(category)]) |tasks| { + tasks.finish(); + } +} + +pub fn isOnCategoryThread(category: Category) bool { + if (categorised_tasks[@intFromEnum(category)]) |tasks| { + const current_thread_id = std.Thread.getCurrentId(); + + for (tasks.threads) |thread| { + if (thread.getCurrentId() == current_thread_id) { + return true; + } + } + } + + return false; +} + +pub fn isOnMainThread() bool { + return std.Thread.getCurrentId() == mainThreadId; +} + +var mainThreadId: std.Thread.Id = 0; + +/// +/// Starts the task pool, marking the thread that it was called from as what it considers to be the "main thread". +/// +pub fn start() std.Thread.SpawnError!void { + categorised_tasks = .{ null, null, null }; + mainThreadId = std.Thread.getCurrentId(); + + if (!builtin.single_threaded) { + if (std.Thread.getCpuCount()) |cpu_count| { + errdefer { + stop(); + } + + inline for (categories, &categorised_tasks) |category, *has_tasks| { + const thread_count = category.getThreadCount(cpu_count); + + if (thread_count != 0) { + has_tasks.* = try Queue.spawn(thread_count); + } + } + } else |cpu_count_error| { + std.log.warn("Failed to get number of CPU cores available: {s}", .{@errorName(cpu_count_error)}); + } + } +} + +/// +/// Stops all threaded task pools. +/// +pub fn stop() void { + inline for (&categorised_tasks) |*has_tasks| { + if (has_tasks.*) |tasks| { + tasks.stop(); + } + + has_tasks.* = null; + } +} diff --git a/src/coral/Tasks/Queue.zig b/src/ona/tasks/Queue.zig similarity index 76% rename from src/coral/Tasks/Queue.zig rename to src/ona/tasks/Queue.zig index f38372a..ed4dce7 100644 --- a/src/coral/Tasks/Queue.zig +++ b/src/ona/tasks/Queue.zig @@ -1,8 +1,8 @@ -const coral = @import("../coral.zig"); +const coral = @import("coral"); const std = @import("std"); -threads_active: std.atomic.Value(usize) = .init(0), +threads: []std.Thread = &.{}, buffer: [max]Runnable = undefined, head: usize = 0, tail: usize = 0, @@ -63,8 +63,6 @@ pub fn finish(self: *Self) void { } fn main(self: *Self) void { - _ = self.threads_active.fetchAdd(1, .monotonic); - while (true) { const runner = self.dequeue(); @@ -75,14 +73,6 @@ fn main(self: *Self) void { runner.call(.{}); self.waiting.finish(); } - - const previous_threads_active = self.threads_active.fetchSub(1, .monotonic); - - std.debug.assert(previous_threads_active != 0); - - if (previous_threads_active == 1) { - coral.heap.allocator.destroy(self); - } } const max = 512; @@ -90,12 +80,19 @@ const max = 512; fn poisonPill() void {} pub fn stop(self: *Self) void { - var threads_remaining = self.threads_active.load(.monotonic); + var threads_remaining = self.threads.len; const poison_pill = Runnable.initFn(poisonPill); while (threads_remaining != 0) : (threads_remaining -= 1) { self.enqueue(poison_pill); } + + for (self.threads) |thread| { + thread.join(); + } + + coral.heap.allocator.free(self.threads); + coral.heap.allocator.destroy(self); } pub fn spawn(thread_count: usize) std.Thread.SpawnError!*Self { @@ -105,12 +102,20 @@ pub fn spawn(thread_count: usize) std.Thread.SpawnError!*Self { const self = try coral.heap.allocator.create(Self); - self.* = .{}; + errdefer { + coral.heap.allocator.destroy(self); + } - var threads_spawned: usize = 0; + self.* = .{ + .threads = try coral.heap.allocator.alloc(std.Thread, thread_count), + }; - while (threads_spawned < thread_count) : (threads_spawned += 1) { - (try std.Thread.spawn(.{}, main, .{self})).detach(); + errdefer { + coral.heap.allocator.free(self.threads); + } + + for (self.threads) |*thread| { + thread.* = try .spawn(.{}, main, .{self}); } return self;