From a788a4fc259b3f4e1036a7fa5c96d30084a2b49b Mon Sep 17 00:00:00 2001 From: kayomn Date: Thu, 23 Oct 2025 07:51:10 +0100 Subject: [PATCH] Asset system v2 --- build.zig | 22 +- build.zig.zon | 4 + src/coral/heap.zig | 44 ++ src/coral/stack.zig | 20 +- src/demos/graphics.zig | 53 +- src/demos/{crt.frag => graphics_crt.frag} | 6 +- src/ona/App.zig | 25 +- src/ona/System.zig | 12 +- src/ona/asset.zig | 416 ++++++-------- src/ona/gfx.zig | 642 +++++++++++----------- src/ona/gfx/device.zig | 435 --------------- src/ona/gfx/glsl.zig | 53 +- src/ona/hid.zig | 6 +- src/ona/ona.zig | 130 ++--- 14 files changed, 698 insertions(+), 1170 deletions(-) rename src/demos/{crt.frag => graphics_crt.frag} (84%) delete mode 100644 src/ona/gfx/device.zig diff --git a/build.zig b/build.zig index 12ab3bf..dbfb3cf 100644 --- a/build.zig +++ b/build.zig @@ -119,7 +119,24 @@ pub fn build(b: *std.Build) void { }), }; - const shaderc_dependency = b.dependency("shaderc_zig", .{}); + const spirv_reflect_dependency = b.dependency("SPIRV-Reflect", .{}); + + const spirv_reflect_lib = b.addLibrary(.{ + .linkage = .static, + .name = "SPIRV-Reflect", + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + }), + }); + + spirv_reflect_lib.addCSourceFile(.{ + .file = spirv_reflect_dependency.path("spirv_reflect.c"), + .flags = &.{ "-Wall", "-Werror" }, + }); + + spirv_reflect_lib.installHeadersDirectory(spirv_reflect_dependency.path("./"), "spirv_reflect", .{}); + spirv_reflect_lib.linkLibC(); const coral_module = b.addModule("coral", .{ .root_source_file = b.path("src/coral/coral.zig"), @@ -141,8 +158,11 @@ pub fn build(b: *std.Build) void { }, }); + const shaderc_dependency = b.dependency("shaderc_zig", .{}); + ona_module.linkLibrary(shaderc_dependency.artifact("shaderc")); ona_module.linkLibrary(config.sdl_dependency.artifact("SDL3")); + ona_module.linkLibrary(spirv_reflect_lib); // config.addShaders(ona_module, &.{ // "./src/ona/gfx/effect_shader.zig", diff --git a/build.zig.zon b/build.zig.zon index 30c36c8..576a49c 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -7,6 +7,10 @@ .url = "git+https://github.com/tiawl/shaderc.zig#69b67221988aa84c91447775ad6157e4e80bab00", .hash = "shaderc_zig-1.0.0-mOl846VjAwDV8YlqQFVvFsWsBa6dLNSiskpTy7lC1hmD", }, + .@"SPIRV-Reflect" = .{ + .url = "https://github.com/KhronosGroup/SPIRV-Reflect/archive/refs/tags/vulkan-sdk-1.3.290.0.tar.gz", + .hash = "N-V-__8AAPjZJwAYPrmeP82KPbIdkLRkudSrnIDw1La5q5pj", + }, .sdl = .{ .url = "git+https://github.com/castholm/SDL.git#b1913e7c31ad72ecfd3ab04aeac387027754cfaf", .hash = "sdl-0.3.0+3.2.22-7uIn9Pg3fwGG2IyIOPxxOSVe-75nUng9clt7tXGFLzMr", diff --git a/src/coral/heap.zig b/src/coral/heap.zig index 59d531e..9ef9eca 100644 --- a/src/coral/heap.zig +++ b/src/coral/heap.zig @@ -2,6 +2,50 @@ const builtin = @import("builtin"); const std = @import("std"); +pub fn Arc(comptime Value: type) type { + const Payload = struct { + ref_count: std.atomic.Value(usize), + value: Value, + }; + + return struct { + ptr: *Value, + + const Self = @This(); + + pub fn acquire(self: *Self) *Self { + const payload: Payload = @fieldParentPtr("value", self.ptr); + + _ = payload.owners_referencing.fetchAdd(1, .monotonic); + + return self; + } + + pub fn init(value: Value) error{OutOfMemory}!Self { + const allocation = try allocator.create(Payload); + + allocation.* = .{ + .ref_count = .init(1), + .value = value, + }; + + return &allocation.value; + } + + pub fn release(self: *Self) ?Value { + const payload: Payload = @fieldParentPtr("value", self.ptr); + + if (payload.ref_count.fetchSub(1, .monotonic) == 1) { + defer { + allocator.destroy(payload); + } + + return payload.value; + } + } + }; +} + pub const allocator = switch (builtin.mode) { .ReleaseFast, .ReleaseSmall => std.heap.smp_allocator, else => debug_allocator.allocator(), diff --git a/src/coral/stack.zig b/src/coral/stack.zig index 897a229..019e738 100644 --- a/src/coral/stack.zig +++ b/src/coral/stack.zig @@ -67,9 +67,13 @@ fn Generic(comptime Item: type, comptime Storage: type) type { const index = self.items.len; - self.items.len = new_len; + self.items.len = std.math.cast(u32, new_len) orelse { + return false; + }; - std.debug.assert(self.items.sliced(index, self.items.len).copy(items)); + for (index..self.items.len, items) |i, item| { + std.debug.assert(self.items.set(i, item)); + } return true; } @@ -85,13 +89,13 @@ fn Generic(comptime Item: type, comptime Storage: type) type { pub fn pushMany(self: *Self, n: usize, item: Item) bool { const new_len = self.items.len + n; - if (new_len > self.cap) { + if (new_len > self.items.cap) { return false; } const offset = self.items.len; - self.items.len = new_len; + self.items.len = @intCast(new_len); for (offset..(offset + n)) |i| { std.debug.assert(self.items.set(i, item)); @@ -100,6 +104,14 @@ fn Generic(comptime Item: type, comptime Storage: type) type { return true; } + pub fn reserve(self: *Self, allocator: std.mem.Allocator, n: usize) std.mem.Allocator.Error!void { + const grow_amount = std.math.sub(usize, n, self.items.cap) catch { + return; + }; + + try self.items.grow(allocator, grow_amount); + } + pub fn set(self: *Self, item: Item) bool { if (self.isEmpty()) { return false; diff --git a/src/demos/graphics.zig b/src/demos/graphics.zig index e98b603..09782ac 100644 --- a/src/demos/graphics.zig +++ b/src/demos/graphics.zig @@ -2,64 +2,45 @@ const ona = @import("ona"); const std = @import("std"); -const CRT = extern struct { - width: f32, - height: f32, +const CRT = struct { time: f32, - padding: [4]u8 = undefined, }; const State = struct { - images: [2]ona.gfx.Images.Handle = [_]ona.gfx.Images.Handle{.default} ** 2, + images: [2]ona.asset.Handle(ona.gfx.Image) = [_]ona.asset.Handle(ona.gfx.Image){.none} ** 2, last_time: f64 = 0, image_index: usize = 0, - crt_effect: ona.gfx.Effects.Handle = .default, - loaded_image: ona.gfx.Images.Handle = .default, + crt_effect: ona.asset.Handle(ona.gfx.Effect) = .none, + loaded_image: ona.asset.Handle(ona.gfx.Image) = .none, }; -fn load(display: ona.Write(ona.gfx.Display), state: ona.Write(State), images: ona.gfx.Images, effects: ona.gfx.Effects) !void { +fn load(display: ona.Write(ona.gfx.Display), state: ona.Write(State), assets: ona.Read(ona.asset.Queue)) !void { display.ptr.size = .{ 1280, 720 }; - state.ptr.loaded_image = try images.load(ona.gfx.QoiImage{ + state.ptr.loaded_image = try assets.ptr.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{ + state.ptr.crt_effect = try assets.ptr.load(ona.gfx.GlslEffect{ .fragment = .{ - .glsl = @embedFile("crt.frag"), - .name = "crt.frag", + .glsl = @embedFile("graphics_crt.frag"), + .name = "graphics_crt.frag", }, }); - errdefer { - effects.unload(state.ptr.crt_effect); - } - - state.ptr.images[0] = try images.load(ona.gfx.CheckerImage{ + state.ptr.images[0] = try assets.ptr.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{ + state.ptr.images[1] = try assets.ptr.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 { @@ -73,14 +54,14 @@ pub fn main() void { .run(); } -fn render(scene: ona.gfx.Scene, state: ona.Read(State), display: ona.Write(ona.gfx.Display), time: ona.Read(ona.App.Time)) void { +fn render(scene: ona.gfx.Scene, state: ona.Read(State), display: ona.Write(ona.gfx.Display), time: ona.Read(ona.App.Time), effects: ona.Write(ona.asset.Store(ona.gfx.Effect))) !void { const width, const height = display.ptr.size; - scene.updateEffect(state.ptr.crt_effect, CRT{ - .width = @floatFromInt(width), - .height = @floatFromInt(height), - .time = @floatCast(time.ptr.elapsed), - }); + if (effects.ptr.modify(state.ptr.crt_effect) catch unreachable) |effect| { + try effect.setProperties(.fragment, CRT{ + .time = @floatCast(time.ptr.elapsed), + }); + } scene.drawSprite(state.ptr.images[state.ptr.image_index], .{ .size = .{ @floatFromInt(width), @floatFromInt(height) }, diff --git a/src/demos/crt.frag b/src/demos/graphics_crt.frag similarity index 84% rename from src/demos/crt.frag rename to src/demos/graphics_crt.frag index 4fce957..c78d8a5 100644 --- a/src/demos/crt.frag +++ b/src/demos/graphics_crt.frag @@ -1,4 +1,8 @@ +properties { + float time; +}; + vec2 crt(vec2 coords, float bend) { vec2 symmetrical_coords = (coords - 0.5) * 2.0; @@ -13,7 +17,7 @@ vec2 crt(vec2 coords, float bend) { 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 float scan = sin(crt_coords.y * scanlineCount + time * 29.0); const vec3 image_color = texture(image, crt_coords).rgb; const vec3 shaded = image_color - vec3(scan * 0.02); diff --git a/src/ona/App.zig b/src/ona/App.zig index 3dff922..42c7186 100644 --- a/src/ona/App.zig +++ b/src/ona/App.zig @@ -96,6 +96,22 @@ pub const Exit = union(enum) { failure: anyerror, }; +pub const Frame = enum(u32) { + _, + + pub fn init(value: u32) Frame { + return @enumFromInt(value); + } + + pub fn increment(self: *Frame) void { + self.* = @enumFromInt(@intFromEnum(self.*) +% 1); + } + + pub fn count(self: Frame) u32 { + return @intFromEnum(self); + } +}; + pub const Path = struct { buffer: [max]u8, unused: u8, @@ -173,8 +189,10 @@ fn scheduleName(comptime schedule: anytype) [:0]const u8 { }; } -pub fn getState(self: *const Self, comptime State: type) ?*State { - if (self.initialized_states.get(.of(State))) |boxed_state| { +pub fn getState(self: Self, comptime State: type) ?*State { + const state_id = coral.TypeId.of(State); + + if (self.initialized_states.get(state_id)) |boxed_state| { return boxed_state.has(State).?; } @@ -187,8 +205,9 @@ pub fn init() error{ OutOfMemory, SystemResources }!Self { .named_systems = .empty, }; - try self.setState(Data{}); try self.setState(Time{}); + try self.setState(Data{}); + try self.setState(Frame.init(1)); try ona.registerChannel(&self, Exit); const data = self.getState(Data).?; diff --git a/src/ona/System.zig b/src/ona/System.zig index ee5abcb..db482db 100644 --- a/src/ona/System.zig +++ b/src/ona/System.zig @@ -110,9 +110,15 @@ pub fn of(comptime function: anytype) *const Self { @compileError(std.fmt.comptimePrint("{s} must have a .init fn to be used as a behavior param", .{@typeName(Param)})); }; - const has_bind_fn = coral.meta.hasFn(Param, "bind"); + const bind_fn = coral.meta.hasFn(Param, "bind"); - inline for (init_fn.params[@intFromBool(has_bind_fn != null)..]) |init_param| { + if (bind_fn) |@"fn"| { + if (@"fn".params.len != 0) { + @compileError(std.fmt.comptimePrint("{s}.bind fn may not accept any parameters to be used as a behavior param", .{@typeName(Param)})); + } + } + + inline for (init_fn.params[@intFromBool(bind_fn != null)..]) |init_param| { const InitParam = coral.meta.UnwrappedOptional(init_param.type orelse { @compileError(std.fmt.comptimePrint("fn {s}.init may not have generic parameters", .{ @typeName(Param), @@ -269,7 +275,7 @@ fn getInitParams(comptime Param: type) []const std.builtin.Type.Fn.Param { } fn usesTrait(comptime Param: type, name: []const u8) bool { - if (@hasDecl(Param, "traits")) { + if (coral.meta.isContainer(@typeInfo(Param)) and @hasDecl(Param, "traits")) { const Traits = @TypeOf(Param.traits); const traits_struct = switch (@typeInfo(Traits)) { diff --git a/src/ona/asset.zig b/src/ona/asset.zig index 3156f85..1199258 100644 --- a/src/ona/asset.zig +++ b/src/ona/asset.zig @@ -4,13 +4,32 @@ const coral = @import("coral"); const std = @import("std"); -pub const GenericHandle = struct { - type_id: *const coral.TypeId, +pub fn Handle(comptime Asset: type) type { + return packed struct(u64) { + index: u32, + salt: u32, + + const Self = @This(); + + pub const none = std.mem.zeroes(Self); + + pub fn generify(self: Self) HandleGeneric { + return .{ + .asset_id = coral.TypeId.of(Asset), + .index = self.index, + .salt = self.salt, + }; + } + }; +} + +const HandleGeneric = struct { index: u32, salt: u32, + asset_id: *const coral.TypeId, - pub fn reify(self: GenericHandle, comptime Asset: type) ?Handle(Asset) { - if (self.type_id != coral.TypeId.of(Asset)) { + pub fn reify(self: HandleGeneric, comptime Asset: type) ?Handle(Asset) { + if (self.asset_id != coral.TypeId.of(Asset)) { return null; } @@ -21,271 +40,188 @@ pub const GenericHandle = struct { } }; -pub fn Handle(comptime Asset: type) type { - return packed struct(u64) { - index: u32, - salt: u32, +pub const Queue = struct { + shared: *Shared, + + const HandlePool = struct { + states: [*]State = undefined, + salts: [*]u32 = undefined, + next: std.atomic.Value(u32) = .init(0), + asset_id: *const coral.TypeId, + cap: u32 = 0, const Self = @This(); - pub const asset_type_id = coral.TypeId.of(Asset); + const State = union(enum) { + vacant: u32, + pending, + occupied, + }; - pub const default: Self = @bitCast(@as(u64, 0)); + fn deinit(self: *Self) void { + coral.heap.allocator.free(self.states[0..self.cap]); + coral.heap.allocator.free(self.salts[0..self.cap]); - pub fn generic(self: Self) GenericHandle { - return .{ - .type_id = asset_type_id, - .index = self.index, - .salt = self.salt, + self.* = .{}; + } + + pub fn isReady(self: Self, handle: HandleGeneric) error{InvalidHandle}!bool { + if (self.asset_id != handle.asset_id) { + return error.InvalidHandle; + } + + if (self.cap < handle.index) { + return false; + } + + if (self.salts[handle.index] != handle.salt) { + return error.InvalidHandle; + } + + return switch (self.states[handle.index]) { + .vacant => error.InvalidHandle, + .pending => null, + .occupied => true, }; } - }; -} -pub fn ResolveQueue(comptime Asset: type) type { - return struct { - arena: std.heap.ArenaAllocator = .init(coral.heap.allocator), + pub fn reserve(self: *Self) HandleGeneric { + while (true) { + const index = self.next.load(.acquire); - tasks: coral.stack.Sequential(struct { - future: *ona.tasks.Future(?Asset), - handle: AssetHandle, - }) = .empty, + if (index < self.cap) { + // Previously-recycled free list available to be used. + const next_free = self.states[index].vacant; - const AssetHandle = Handle(Asset); + if (self.next.cmpxchgWeak(index, next_free, .acq_rel, .acquire) != null) { + // CAS failed, loop again + continue; + } - fn ResolveTask(comptime Loadable: type) type { - const load_fn_name = "load"; + self.states[index] = .pending; - 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, - })); - }; - - 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 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, + return .{ + .salt = self.salts[index], + .asset_id = self.asset_id, + .index = index, }; - - 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; } + // The memory for this resource will be created in the end-of-frame asset sync elsewhere, where the asset + // buffers are grown to equal next again. return .{ - .index = self.free_index, - .salt = entry.salt, + .index = self.next.fetchAdd(1, .monotonic), + .asset_id = self.asset_id, + .salt = 1, }; } + } + }; - const handle = AssetHandle{ - .index = self.asset_entries.items.len, - .salt = 1, - }; + const Shared = struct { + asset_handle_pools: coral.tree.Binary(*const coral.TypeId, HandlePool, coral.tree.scalarTraits(*const coral.TypeId)) = .empty, + }; - try self.asset_entries.pushGrow(coral.heap.allocator, .{ - .value = undefined, + pub fn deinit(self: *Queue) void { + self.shared.asset_handle_pools.deinit(coral.heap.allocator); + } - .entry = .{ - .usage = .reserved, - .salt = handle.salt, - }, + pub fn load(self: Queue, loadable: anytype) error{OutOfMemory}!Handle(coral.meta.UnwrappedError(@typeInfo(@TypeOf(@TypeOf(loadable).load)).@"fn".return_type.?)) { + const Asset = coral.meta.UnwrappedError(@typeInfo(@TypeOf(@TypeOf(loadable).load)).@"fn".return_type.?); + + const handles = self.shared.asset_handle_pools.get(.of(Asset)) orelse { + std.debug.panic("Asset type {s} must be registered first before attempting to load an instance", .{ + @typeName(Asset), }); + }; - self.free_index += 1; + const handle = handles.reserve().reify(Asset).?; + + return handle; + } + + fn register(self: *Queue, asset_id: *const coral.TypeId) error{OutOfMemory}!*HandlePool { + if (try self.shared.asset_handle_pools.insert(coral.heap.allocator, asset_id, .{ .asset_id = asset_id })) |handles| { + return handles; + } + + return self.shared.asset_handle_pools.get(asset_id).?; + } +}; + +pub fn Store(comptime Asset: type) type { + return struct { + handles: *Queue.HandlePool, + assets: coral.stack.Sequential(Asset) = .empty, + + const Self = @This(); + + pub fn deinit(self: *Self) void { + self.assets.deinit(coral.heap.allocator); + + self.* = undefined; + } + + pub fn insert(self: *Self, asset: Asset) error{OutOfMemory}!Handle(Asset) { + const handle = self.handles.reserve().reify(Asset).?; + + if (handle.index < self.assets.items.len) { + self.assets.items[handle.index] = asset; + } else { + try self.assets.pushGrow(asset); + } return handle; } - 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]; + pub fn modify(self: *Self, handle: Handle(Asset)) error{InvalidHandle}!?*Asset { + // TODO: Implement. + _ = self; + _ = handle; - if (entry.usage != .reserved) { - return false; - } - - if (reserved_handle.salt != entry.salt) { - return false; - } - - if (resolved_asset) |asset| { - entry.usage = .occupied; - self.asset_entries.items.slice(.value)[index] = .{ .asset = asset }; - } else { - entry.usage = .missing; - } - - return true; + return null; } }; } -pub const extension_len_max = 7; +pub fn register(app: *ona.App, comptime Asset: type) !void { + const AssetStore = Store(Asset); + + const assets = struct { + fn integrate(store: ona.Exclusive(AssetStore)) !void { + const required_cap = store.ptr.handles.next.load(.monotonic); + + if (store.ptr.handles.cap < required_cap) { + store.ptr.handles.states = (try coral.heap.allocator.realloc(store.ptr.handles.states[0..store.ptr.handles.cap], required_cap)).ptr; + store.ptr.handles.salts = (try coral.heap.allocator.realloc(store.ptr.handles.salts[0..store.ptr.handles.cap], required_cap)).ptr; + + @memset(store.ptr.handles.salts[store.ptr.handles.cap..required_cap], 1); + @memset(store.ptr.handles.states[store.ptr.handles.cap..required_cap], .pending); + + store.ptr.handles.cap = required_cap; + } + + try store.ptr.assets.reserve(coral.heap.allocator, required_cap); + + std.debug.assert(store.ptr.assets.pushMany(required_cap - store.ptr.assets.items.len, undefined)); + } + }; + + const queue = app.getState(Queue).?; + + try app.setState(AssetStore{ + .handles = try queue.register(.of(Asset)), + }); + + try app.on(.post_update, .of(assets.integrate)); +} + +pub fn setup(app: *ona.App) void { + var arc_queue = try coral.heap.Arc(Queue).init(.{}); + + errdefer { + arc_queue.release().?.deinit(); + } + + _ = app.setState(try Queue.init()); +} diff --git a/src/ona/gfx.zig b/src/ona/gfx.zig index 0e0b824..0e8ca85 100644 --- a/src/ona/gfx.zig +++ b/src/ona/gfx.zig @@ -1,11 +1,13 @@ pub const Color = @import("gfx/Color.zig"); +const c = @cImport({ + @cInclude("spirv_reflect/spirv_reflect.h"); +}); + const builtin = @import("builtin"); const coral = @import("coral"); -const device = @import("gfx/device.zig"); - const glsl = @import("gfx/glsl.zig"); const ona = @import("ona.zig"); @@ -15,337 +17,225 @@ const qoi = @import("gfx/qoi.zig"); const std = @import("std"); pub const CheckerImage = struct { - square_size: u32, + square_size: usize, colors: [2]Color, - width: u32, - height: u32, + width: usize, + height: usize, - pub fn load(self: CheckerImage) !device.Image { - var image = try device.Image.init(self.width, self.height, .rgba8, .{ .is_input = true }); + pub fn load(self: CheckerImage) !Image { + var image = Image.init(self.width, self.height, .rgba8); errdefer { image.deinit(); } - const image_size = image.height * image.width * device.Image.Format.rgba8.stride(); + if (self.square_size == 0) { + try image.fill(&self.colors[0].toRgba8()); + } 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; - { - var image_memory = try device.Memory.init(image_size); - - defer { - image_memory.deinit(); + try coral.bytes.writeAll(image.writer(), &self.colors[color_index].toRgba8()); } - - 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 { size: [2]u16, is_hidden: bool, }; -pub const Effects = ona.Assets(device.Effect); +pub const Effect = struct { + shaders: [stages.len]c.SpvReflectShaderModule, + properties_buffers: [stages.len][]u8, -pub const Images = ona.Assets(device.Image); + pub const Stage = enum { + vertex, + fragment, + }; -pub const QoiImage = struct { - path: ona.App.Path, + pub const Spirv = struct { + vertex_code: []const u8 = &.{}, + fragment_code: []const u8 = &.{}, + }; - 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)); + pub const SpirvError = error{ + OutOfMemory, + InvalidVertexShader, + InvalidFragmentShader, + }; - defer { - coral.heap.allocator.free(qoi_data.bytes); - } + pub fn deinit(self: *Effect) void { + for (&self.shaders, self.properties_buffers) |*shader, properties_buffer| { + c.spvReflectDestroyShaderModule(shader); - 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); + if (properties_buffer.len != 0) { + coral.heap.allocator.free(properties_buffer); } } - image_memory.commit(); + self.* = undefined; + } - var image = try device.Image.init(header.width, header.height, .rgba8, .{ - .is_input = true, - }); + pub fn init(spirv: Spirv) SpirvError!Effect { + var effect = Effect{ + .shaders = .{ .{}, .{} }, + .properties_buffers = .{ &.{}, &.{} }, + }; errdefer { - image.deinit(); + effect.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, - }; - - 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, + try switch (c.spvReflectCreateShaderModule(spirv.vertex_code.len, spirv.vertex_code.ptr, &effect.shaders[@intFromEnum(Stage.vertex)])) { + c.SPV_REFLECT_RESULT_SUCCESS => {}, + c.SPV_REFLECT_RESULT_ERROR_ALLOC_FAILED => error.OutOfMemory, + else => error.InvalidVertexShader, }; + + try switch (c.spvReflectCreateShaderModule(spirv.fragment_code.len, spirv.fragment_code.ptr, &effect.shaders[@intFromEnum(Stage.fragment)])) { + c.SPV_REFLECT_RESULT_SUCCESS => {}, + c.SPV_REFLECT_RESULT_ERROR_ALLOC_FAILED => error.OutOfMemory, + else => error.InvalidFragmentShader, + }; + + return effect; } + + pub fn setProperties(self: *Effect, stage: Stage, properties: anytype) error{ OutOfMemory, InvalidData }!void { + const Properties = @TypeOf(properties); + + const properties_struct = switch (@typeInfo(Properties)) { + .@"struct" => |@"struct"| @"struct", + + else => @compileError(std.fmt.comptimePrint("`properties` must be a struct type, not {s}", .{ + @typeName(Properties), + })), + }; + + if (properties_struct.is_tuple) { + @compileError(std.fmt.comptimePrint("`properties` must be a non-type struct type, not {s}", .{ + @typeName(Properties), + })); + } + + const stage_index = @intFromEnum(stage); + + const properties_binding = spirvDescriptorBindingNamed(self.shaders[stage_index], "Effect") orelse { + return error.InvalidData; + }; + + const properties_buffer = &self.properties_buffers[stage_index]; + + if (properties_buffer.len != properties_binding.block.padded_size) { + properties_buffer.* = try coral.heap.allocator.realloc(properties_buffer.*, properties_binding.block.padded_size); + + @memset(properties_buffer.*, 0); + } + + inline for (properties_struct.fields) |properties_field| { + const properties_variable = spirvBlockVariableNamed(properties_binding.block, properties_field.name) orelse { + return error.InvalidData; + }; + + switch (properties_field.type) { + f32 => { + if (properties_variable.type_description.*.op != c.SpvOpTypeFloat) { + return error.InvalidData; + } + + if (properties_variable.numeric.scalar.width != @bitSizeOf(f32)) { + return error.InvalidData; + } + + const property_bytes = std.mem.asBytes(&@field(properties, properties_field.name)); + + @memcpy(properties_buffer.*[properties_variable.offset .. properties_variable.offset + property_bytes.len], property_bytes); + }, + + else => { + @compileError(std.fmt.comptimePrint("Unsupported property type for {s} field '{s}': {s}.", .{ + @typeName(Properties), + properties_field.name, + @typeName(properties_field.type), + })); + }, + } + } + } + + fn spirvBlockVariableNamed(block: c.SpvReflectBlockVariable, name: []const u8) ?c.SpvReflectBlockVariable { + var remaining = block.member_count; + + while (remaining != 0) : (remaining -= 1) { + const block_variable = block.members[remaining]; + + if (std.mem.eql(u8, std.mem.span(block_variable.name), name)) { + return block_variable; + } + } + + return null; + } + + fn spirvDescriptorBindingNamed(shader: c.SpvReflectShaderModule, name: []const u8) ?c.SpvReflectDescriptorBinding { + var remaining = shader.descriptor_binding_count; + + while (remaining != 0) : (remaining -= 1) { + const descriptor_binding = shader.descriptor_bindings[remaining]; + + if (std.mem.eql(u8, std.mem.span(descriptor_binding.name), name)) { + return descriptor_binding; + } + } + + return null; + } + + const stages = std.enums.values(Stage); }; pub const GlslEffect = struct { - fragment: Source = .empty, - vertex: Source = .empty, + fragment: glsl.Source = .empty, + vertex: glsl.Source = .empty, - pub const Source = glsl.Assembly.Source; + const Camera = extern struct { + projection: [4]@Vector(4, f32), + }; - pub fn load(self: GlslEffect) !device.Effect { + const Self = @This(); + + pub fn load(self: Self) !Effect { var arena = std.heap.ArenaAllocator.init(coral.heap.allocator); defer { arena.deinit(); } - var assembly = try glsl.Assembly.init(.spirv, switch (builtin.mode) { + var target = try glsl.Target.init(.spirv, switch (builtin.mode) { .ReleaseSafe, .Debug => .unoptimized, .ReleaseFast => .optimize_speed, .ReleaseSmall => .optimize_size, }); defer { - assembly.deinit(); + target.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; + const camera_block = glsl.nativeUniformBlock(1, Camera); - return device.Effect.init(.{ - .fragment_code = try assembly.assemble(arena_allocator, .fragment, .{ + return .init(.{ + .fragment_code = try target.assemble(arena_allocator, .fragment, .{ .glsl = try glsl.inject(arena_allocator, switch (self.fragment.glsl.len) { 0 => @embedFile("gfx/canvas.frag"), else => self.fragment.glsl, @@ -354,8 +244,8 @@ pub const GlslEffect = struct { .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, + .properties = glsl.PreProcessor{ .substitution = "layout (binding = 0) uniform Effects" }, + .camera = camera_block, }), .name = switch (self.fragment.name.len) { @@ -364,7 +254,7 @@ pub const GlslEffect = struct { }, }), - .vertex_code = try assembly.assemble(arena_allocator, .vertex, .{ + .vertex_code = try target.assemble(arena_allocator, .vertex, .{ .glsl = try glsl.inject(arena_allocator, switch (self.vertex.glsl.len) { 0 => @embedFile("gfx/canvas.vert"), else => self.vertex.glsl, @@ -382,8 +272,8 @@ pub const GlslEffect = struct { .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, + .properties = glsl.PreProcessor{ .substitution = "layout (binding = 0) uniform Effects" }, + .camera = camera_block, }), .name = switch (self.vertex.name.len) { @@ -395,61 +285,161 @@ pub const GlslEffect = struct { } }; -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(); +pub const Image = struct { + data: coral.stack.Sequential(u8), + width: usize, + height: usize, + format: Format, + + /// + /// The way that image data should be stored in memory. + /// + pub const Format = enum { + rgba8, + + pub fn stride(self: Format) usize { + return switch (self) { + .rgba8 => @sizeOf([4]u8), + }; + } + }; + + /// + /// Cleans up all resources associated with `self`. + /// + pub fn deinit(self: *Image) void { + self.data.deinit(coral.heap.allocator); } - context.ptr.render(); -} + /// + /// Attempts to fill `self` with `fill_data`. + /// + /// **Note** passing a `fill_data.len` not equal to the format stride will result in `error.InvalidData`. + /// + pub fn fill(self: *Image, fill_data: []const u8) error{ InvalidData, OutOfMemory }!void { + const format_stride = self.format.stride(); + + if (fill_data.len != format_stride) { + return error.InvalidData; + } + + var unfilled = self.width * self.height * format_stride; + + try self.data.reserve(coral.heap.allocator, unfilled); + + self.data.clear(); + + while (unfilled != 0) : (unfilled -= 1) { + std.debug.assert(self.data.pushAll(fill_data)); + } + } + + /// + /// Return a new `Image` with the given `width`, `height`, and `format`. + /// + pub fn init(width: usize, height: usize, format: Format) Image { + return .{ + .data = .empty, + .width = width, + .height = height, + .format = format, + }; + } + + /// + /// Write `data` into `self` from the last location written to, returning the number of bytes written. + /// + /// If `data` overflows the image data buffer, zero bytes of data are written and `0` is always returned. + /// + pub fn write(self: *Image, data: []const u8) usize { + self.data.reserve(coral.heap.allocator, self.width * self.height * self.format.stride()) catch { + return 0; + }; + + if (!self.data.pushAll(data)) { + return 0; + } + + return data.len; + } + + /// + /// Returns a `coral.bytes.Writable` for writing to `self`. + /// + pub fn writer(self: *Image) coral.bytes.Writable { + return .initRef(self, write); + } +}; + +pub const QoiImage = struct { + path: ona.App.Path, + + pub fn load(self: QoiImage, data: ona.App.Data) !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 = Image.init(header.width, header.height, .rgba8); + + defer { + image.deinit(); + } + + { + var decoder = qoi.DecodeStream.init(header); + + while (try decoder.fetch(qoi_data.reader())) |run| { + try coral.bytes.writeN(image.writer(), &run.pixel.bytes(), run.count); + } + } + + return image; + } +}; + +pub const Scene = struct { + pub const Rect = struct { + left: f32, + top: f32, + right: f32, + bottom: f32, + }; + + 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: ona.asset.Handle(Effect) = .none, + }; + + pub fn drawSprite(self: Scene, image: ona.asset.Handle(Image), draw: SpriteDraw) void { + _ = self; + _ = image; + _ = draw; + } + + pub fn init() Scene { + return .{}; + } +}; pub fn setup(app: *ona.App) !void { - try device.start(); - - errdefer { - device.stop(); - } - - var arena = std.heap.ArenaAllocator.init(coral.heap.allocator); - - defer { - arena.deinit(); - } - const default_width, const default_height = .{ 1280, 720 }; - { - var swapchain = try device.Swapchain.init(default_width, default_height, "Ona"); - - errdefer { - swapchain.deinit(); - } - - var backbuffer = try device.Target.init(default_width, default_height); - - errdefer { - backbuffer.deinit(); - } - - try app.setState(Context{ - .swapchain = swapchain, - .backbuffer = backbuffer, - }); - } - 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(); + try ona.asset.register(app, Image); + try ona.asset.register(app, Effect); + // try app.on(.pre_update, .of(renderFrame)); + //try app.on(.post_update, .of(swapBuffers)); } diff --git a/src/ona/gfx/device.zig b/src/ona/gfx/device.zig deleted file mode 100644 index aecfa6f..0000000 --- a/src/ona/gfx/device.zig +++ /dev/null @@ -1,435 +0,0 @@ -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 4123456..6194d7a 100644 --- a/src/ona/gfx/glsl.zig +++ b/src/ona/gfx/glsl.zig @@ -6,10 +6,23 @@ const coral = @import("coral"); const std = @import("std"); +/// +/// 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 = "", + }; +}; + /// /// State machine for compiling GLSL shader source code into targeted executable formats. /// -pub const Assembly = struct { +pub const Target = struct { compiler: c.shaderc_compiler_t, options: c.shaderc_compile_options_t, @@ -22,19 +35,6 @@ 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. /// @@ -44,9 +44,9 @@ pub const Assembly = struct { }; /// - /// Target format used for shader binaries. + /// Backend format used for shader binaries. /// - pub const Target = enum { + pub const Backend = enum { spirv, }; @@ -66,7 +66,7 @@ pub const Assembly = struct { /// **Note** the shader assumes the entry-point of the GLSL program matches the signature `void main()` /// pub fn assemble( - self: Assembly, + self: Target, allocator: std.mem.Allocator, stage: Stage, source: Source, @@ -102,7 +102,7 @@ pub const Assembly = struct { }; } - pub fn deinit(self: *Assembly) void { + pub fn deinit(self: *Target) void { c.shaderc_compile_options_release(self.options); c.shaderc_compiler_release(self.compiler); @@ -110,14 +110,14 @@ pub const Assembly = struct { } /// - /// Returns an `Assembly` with `target` as the desired compilation format and `optimization` as the desired + /// Returns an `Target` with `backend` as the desired compilation format and `optimization` as the desired /// optimization level. /// /// Should not be enough memory be available, `error.OutOfMemory` is returned instead. /// /// **Note** optimizations are not guaranteed and will also depend on the `target` specified. /// - pub fn init(target: Target, optimizations: Optimizations) error{OutOfMemory}!Assembly { + pub fn init(backend: Backend, optimizations: Optimizations) error{OutOfMemory}!Target { const compiler = c.shaderc_compiler_initialize() orelse { return error.OutOfMemory; }; @@ -134,7 +134,7 @@ pub const Assembly = struct { c.shaderc_compile_options_release(options); } - switch (target) { + switch (backend) { .spirv => { c.shaderc_compile_options_set_target_env(options, c.shaderc_target_env_vulkan, c.shaderc_env_version_vulkan_1_1); }, @@ -207,6 +207,17 @@ pub const Output = struct { } }; +pub const PreProcessor = struct { + substitution: []const u8, + + pub fn serialize(self: PreProcessor, name: []const u8, output: coral.bytes.Writable) coral.bytes.ReadWriteError!void { + try coral.bytes.writeFormatted(output, "#define {name} {substitution}\n", .{ + .name = name, + .substitution = self.substitution, + }); + } +}; + pub const Primitive = enum { float, uint, diff --git a/src/ona/hid.zig b/src/ona/hid.zig index 35cf25a..04e5add 100644 --- a/src/ona/hid.zig +++ b/src/ona/hid.zig @@ -200,13 +200,13 @@ pub const KeyScancode = enum(u32) { _, }; -pub fn poll(exit: ona.Send(ona.App.Exit), hid_events: ona.Send(ona.hid.Event)) !void { +pub fn poll(exit: ona.Write(ona.Channel(ona.App.Exit)), hid_events: ona.Write(ona.Channel(ona.hid.Event))) !void { var event: c.SDL_Event = undefined; while (c.SDL_PollEvent(&event)) { - try hid_events.push(switch (event.type) { + try hid_events.ptr.push(switch (event.type) { c.SDL_EVENT_QUIT => { - return exit.push(.success); + return exit.ptr.push(.success); }, c.SDL_EVENT_KEY_UP => .{ .key_up = @enumFromInt(event.key.scancode) }, diff --git a/src/ona/ona.zig b/src/ona/ona.zig index dc2bc9d..d9c8603 100644 --- a/src/ona/ona.zig +++ b/src/ona/ona.zig @@ -4,7 +4,11 @@ pub const Setup = @import("Setup.zig"); pub const System = @import("System.zig"); -const asset = @import("asset.zig"); +pub const asset = @import("asset.zig"); + +const c = @cImport({ + @cInclude("SDL3/SDL.h"); +}); const coral = @import("coral"); @@ -16,52 +20,7 @@ const std = @import("std"); pub const tasks = @import("tasks.zig"); -pub fn Assets(comptime Asset: type) type { - return struct { - app_data: App.Data, - resolved: *Store, - resolves: *ResolveQueue, - - pub const Handle = asset.Handle(Asset); - - const ResolveQueue = asset.ResolveQueue(Asset); - - const Self = @This(); - - 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 .{ - .resolved = resolved, - .resolves = resolves, - .app_data = app_data.*, - }; - } - - pub fn load(self: Self, loadable: anytype) error{OutOfMemory}!Handle { - const reserved_handle = try self.resolved.reserve(); - - 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 unload(self: Self, handle: Handle) void { - // TODO: Implement. - _ = self.resolved.remove(handle); - } - }; -} - -fn Channel(comptime Message: type) type { +pub fn Channel(comptime Message: type) type { return struct { buffers: [2]coral.stack.Sequential(Message) = .{ .empty, .empty }, swap_index: u1 = 0, @@ -85,11 +44,11 @@ fn Channel(comptime Message: type) type { } } - fn messages(self: Self) []const Message { + pub fn messages(self: Self) []const Message { return self.buffers[self.swap_index ^ 1].items.slice(); } - fn push(self: *Self, message: Message) std.mem.Allocator.Error!void { + pub fn push(self: *Self, message: Message) std.mem.Allocator.Error!void { try self.buffers[self.swap_index].pushGrow(coral.heap.allocator, message); } }; @@ -189,47 +148,29 @@ pub fn Read(comptime State: type) type { }; } -pub fn Receive(comptime Message: type) type { - const MessageChannel = Channel(Message); +pub const Ticks = struct { + last: u32, + now: u32, - return struct { - has_channel: ?*const MessageChannel, + pub fn bind() App.Frame { + return .init(0); + } - const Self = @This(); - - pub fn init(channel: ?*const MessageChannel) Self { - return .{ - .channel = channel, - }; + pub fn init(previous_frame: *App.Frame, current_frame: *const App.Frame) Ticks { + defer { + previous_frame.* = current_frame.*; } - pub fn messages(self: Self) []const Message { - if (self.has_channel) |channel| { - return channel.messages(); - } - } - }; -} + return .{ + .last = previous_frame.count(), + .now = current_frame.count(), + }; + } -pub fn Send(comptime Message: type) type { - const MessageChannel = Channel(Message); - - return struct { - channel: *MessageChannel, - - const Self = @This(); - - pub fn init(channel: *MessageChannel) Self { - return .{ - .channel = channel, - }; - } - - pub fn push(self: Self, message: Message) std.mem.Allocator.Error!void { - try self.channel.push(message); - } - }; -} + pub fn isNewer(self: Ticks, tick: u32) bool { + return tick > self.last and tick <= self.now; + } +}; pub fn Write(comptime State: type) type { return struct { @@ -253,6 +194,7 @@ fn realtimeLoop(app: *App) !void { const updates_per_frame = 60.0; const target_frame_time = 1.0 / updates_per_frame; const time = app.getState(App.Time).?; + const current_frame = app.getState(App.Frame).?; const exit_channel = app.getState(Channel(App.Exit)).?; try tasks.start(); @@ -263,16 +205,15 @@ fn realtimeLoop(app: *App) !void { try app.run(.load); - const ticks_initial = std.time.milliTimestamp(); - var ticks_previous = ticks_initial; + var ticks_previous = c.SDL_GetTicks(); var accumulated_time = @as(f64, 0); - while (true) { - const ticks_current = std.time.milliTimestamp(); - const milliseconds_per_second = 1000.0; + while (true) : (current_frame.increment()) { + const ticks_current = c.SDL_GetTicks(); + const ticks_per_second = 1000.0; - accumulated_time += @as(f64, @floatFromInt(ticks_current - ticks_previous)) / milliseconds_per_second; - time.elapsed = @as(f64, @floatFromInt(ticks_current - ticks_initial)) / milliseconds_per_second; + accumulated_time += @as(f64, @floatFromInt(ticks_current - ticks_previous)) / ticks_per_second; + time.elapsed = @as(f64, @floatFromInt(ticks_current)) / ticks_per_second; ticks_previous = ticks_current; try app.run(.pre_update); @@ -301,11 +242,6 @@ fn realtimeLoop(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);