Asset system v2

This commit is contained in:
kayomn 2025-10-23 07:51:10 +01:00
parent be4972e75e
commit a788a4fc25
14 changed files with 698 additions and 1170 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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(),

View File

@ -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;

View File

@ -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) },

View File

@ -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);

View File

@ -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).?;

View File

@ -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)) {

View File

@ -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());
}

View File

@ -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));
}

View File

@ -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;
}

View File

@ -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,

View File

@ -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) },

View File

@ -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);