Add graphics command queues
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
kayomn 2025-08-08 13:52:45 +01:00
parent 1a03f1585f
commit 8c447eac1a
13 changed files with 554 additions and 347 deletions

View File

@ -12,6 +12,8 @@ pub const heap = @import("./heap.zig");
pub const map = @import("./map.zig");
pub const meta = @import("./meta.zig");
pub const scalars = @import("./scalars.zig");
pub const stack = @import("./stack.zig");
@ -113,15 +115,26 @@ pub fn KeyValuePair(comptime Key: type, comptime Value: type) type {
};
}
pub fn require(function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) switch (@typeInfo(@typeInfo(@TypeOf(function)).@"fn".return_type.?)) {
.error_union => |error_union| error_union.payload,
else => @compileError("fn parameter `function` must return an error union"),
} {
return @call(.auto, function, args) catch |call_error| {
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
pub fn expect(function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) meta.Unwrapped(@typeInfo(@TypeOf(function)).@"fn".return_type.?) {
const Returns = @typeInfo(@TypeOf(function)).@"fn".return_type.?;
std.debug.panic("requirement failed: {s}", .{@errorName(call_error)});
return switch (@typeInfo(Returns)) {
.error_union => @call(.auto, function, args) catch |call_error| {
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
std.debug.panic("requirement failed: {s}", .{@errorName(call_error)});
},
.optional => @call(.auto, function, args) orelse {
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
@panic("requirement failed: null");
},
else => @call(.auto, function, args),
};
}

35
src/coral/meta.zig Normal file
View File

@ -0,0 +1,35 @@
const std = @import("std");
pub fn Unwrapped(comptime Value: type) type {
return switch (@typeInfo(Value)) {
.error_union => |error_union| error_union.payload,
.optional => |optional| optional.child,
else => Value,
};
}
pub fn isContainer(@"type": std.builtin.Type) bool {
return switch (@"type") {
.@"struct", .@"union", .@"enum", .@"opaque" => true,
else => false,
};
}
pub fn hasFn(comptime Type: type, comptime fn_name: []const u8) ?std.builtin.Type.Fn {
const @"type" = @typeInfo(Type);
if (!isContainer(@"type")) {
return null;
}
if (!@hasDecl(Type, fn_name)) {
return null;
}
const Fn = @TypeOf(@field(Type, fn_name));
return switch (@typeInfo(Fn)) {
.@"fn" => |@"fn"| @"fn",
else => null,
};
}

View File

@ -6,5 +6,10 @@ pub fn main() void {
ona.realtime_app
.with(.initModule(ona.hid))
.with(.initModule(ona.gfx))
.with(.initSystem(.render, .of(render)))
.run();
}
fn render(commands: ona.gfx.Commands) void {
_ = commands;
}

View File

@ -65,7 +65,7 @@ pub fn hasState(self: *Self, comptime State: type) ?*State {
return null;
}
pub fn init() std.mem.Allocator.Error!Self {
pub fn init() error{OutOfMemory}!Self {
var self = Self{
.initialized_states = .empty,
.named_systems = .empty,
@ -80,19 +80,13 @@ pub fn init() std.mem.Allocator.Error!Self {
return self;
}
pub fn on(self: *Self, comptime schedule: anytype, behavior: *const Behavior) std.mem.Allocator.Error!void {
pub fn on(self: *Self, comptime schedule: anytype, behavior: *const Behavior) error{OutOfMemory}!void {
const schedule_name = scheduleName(schedule);
const systems = self.named_systems.get(schedule_name) orelse (try self.named_systems.insert(coral.heap.allocator, schedule_name, .empty)).?;
try systems.insert(self, behavior);
}
pub fn putState(self: *Self, state: anytype) std.mem.Allocator.Error!*@TypeOf(state) {
try self.setState(state);
return self.hasState(@TypeOf(state)).?;
}
pub fn run(self: *Self, tasks: *coral.asio.TaskQueue, comptime schedule: anytype) RunError!void {
const schedule_name = scheduleName(schedule);
const systems = self.named_systems.get(schedule_name) orelse (try self.named_systems.insert(coral.heap.allocator, schedule_name, .empty)).?;
@ -100,7 +94,7 @@ pub fn run(self: *Self, tasks: *coral.asio.TaskQueue, comptime schedule: anytype
try systems.run(self, tasks);
}
pub fn setState(self: *Self, state: anytype) std.mem.Allocator.Error!void {
pub fn setState(self: *Self, state: anytype) error{OutOfMemory}!void {
var boxed_state = try coral.Box.init(coral.heap.allocator, state);
errdefer {

View File

@ -65,13 +65,17 @@ const Param = struct {
@compileError("generic behavior params are not permitted");
};
const init_fn = hasFn(FnParam, "init", null) orelse {
const init_fn = coral.meta.hasFn(FnParam, "init") orelse {
@compileError(std.fmt.comptimePrint("{s} must contain a .init declaration", .{@typeName(FnParam)}));
};
comptime var uses_bind = false;
if (hasFn(FnParam, "bind", &.{})) |bind_fn| {
if (coral.meta.hasFn(FnParam, "bind")) |bind_fn| {
if (bind_fn.params.len != 0) {
@compileError(std.fmt.comptimePrint("{s}.bind cannot accept any parameters", .{@typeName(FnParam)}));
}
const State = switch (@typeInfo(bind_fn.return_type.?)) {
.error_union => |error_union| error_union.payload,
else => bind_fn.return_type.?,
@ -90,10 +94,13 @@ const Param = struct {
uses_bind = true;
}
if (init_fn.return_type != FnParam) {
if (coral.meta.Unwrapped(init_fn.return_type.?) != FnParam) {
const param_type_name = @typeName(FnParam);
@compileError(std.fmt.comptimePrint("Fn {s}.init must return {s}", .{ param_type_name, param_type_name }));
@compileError(std.fmt.comptimePrint("Fn {s}.init must return some variation of {s}", .{
param_type_name,
param_type_name,
}));
}
return .{
@ -143,54 +150,6 @@ pub fn before(comptime self: *const Self, comptime dependant: *const Self) *cons
return &afters.instance;
}
fn hasFn(comptime SystemParam: type, comptime name: []const u8, comptime has_expected_param_types: ?[]const type) ?std.builtin.Type.Fn {
switch (@typeInfo(SystemParam)) {
.@"struct", .@"opaque" => {},
else => @compileError(std.fmt.comptimePrint("{s} must be a struct or opaque types", .{
@typeName(SystemParam),
})),
}
if (@hasDecl(SystemParam, name)) {
const named_fn = switch (@typeInfo(@TypeOf(@field(SystemParam, name)))) {
.@"fn" => |@"fn"| @"fn",
else => @compileError(std.fmt.comptimePrint("{s}.{s} declaration must be made an fn type", .{
@typeName(SystemParam),
name,
})),
};
if (has_expected_param_types) |expected_param_types| {
if (named_fn.params.len != expected_param_types.len) {
@compileError(std.fmt.comptimePrint("{s}.{s} fn must accept {d} parameters, not {d}", .{
@typeName(SystemParam),
name,
expected_param_types.len,
named_fn.params.len,
}));
}
inline for (named_fn.params, expected_param_types, 1..expected_param_types.len + 1) |fn_param, ExpectedParam, i| {
if (fn_param.type != ExpectedParam) {
@compileError(std.fmt.comptimePrint("Parameter {d} of {s}.{s} fn must be {s}, not {s}", .{
i,
@typeName(SystemParam),
name,
@typeName(ExpectedParam),
@typeName(fn_param.type.?),
}));
}
}
}
return named_fn;
}
return null;
}
pub fn of(comptime call: anytype) *const Self {
const Call = @TypeOf(call);
@ -255,17 +214,16 @@ pub fn of(comptime call: anytype) *const Self {
}
}
call_args[i] = @call(.auto, fn_param.type.?.init, init_args);
call_args[i] = coral.expect(fn_param.type.?.init, init_args);
}
switch (@typeInfo(call_fn.return_type.?)) {
.void => @call(.auto, call, call_args),
.error_union => coral.require(call, call_args),
else => @compileError(std.fmt.comptimePrint("parameter `call` must be fn (*ona.App) void or fn (*ona.App) !void, not {s}", .{
if (coral.meta.Unwrapped(call_fn.return_type.?) != void) {
@compileError(std.fmt.comptimePrint("parameter `call` must return some variation of void", .{
@typeName(Call),
})),
}));
}
coral.expect(call, call_args);
}
const instance = Self{

View File

@ -29,7 +29,7 @@ pub fn init(comptime start: anytype) Self {
switch (@typeInfo(start_fn.return_type.?)) {
.error_union => |error_union| switch (error_union.payload) {
void => coral.require(start, .{app}),
void => coral.expect(start, .{app}),
else => @compileError(error_message),
},
@ -52,12 +52,22 @@ pub fn initModule(comptime namespace: anytype) Self {
return init(namespace.setup);
}
pub fn initSystem(comptime schedule: anytype, comptime behavior: *const ona.App.Behavior) Self {
const system_initialization = struct {
fn apply(app: *ona.App) !void {
try app.on(schedule, behavior);
}
};
return init(system_initialization.apply);
}
pub fn run(self: Self) void {
defer {
coral.heap.traceLeaks();
}
var app = coral.require(ona.App.init, .{});
var app = coral.expect(ona.App.init, .{});
defer {
app.deinit();

View File

@ -1,8 +1,8 @@
const builtin = @import("builtin");
const Context = @import("./gfx/Context.zig");
const coral = @import("coral");
const ext = @cImport({
const c = @cImport({
@cInclude("SDL3/SDL.h");
@cInclude("shaderc/shaderc.h");
});
@ -13,72 +13,14 @@ const ona = @import("./ona.zig");
const std = @import("std");
const Context = struct {
window: *ext.SDL_Window,
frame_arena: std.heap.ArenaAllocator,
gpu_device: *ext.SDL_GPUDevice,
shader_compiler: ext.shaderc_compiler_t,
spirv_options: ext.shaderc_compile_options_t,
pub const Commands = struct {
pending: *Context.Queue,
pub const AssembleError = std.mem.Allocator.Error || error{BadSyntax};
pub const AssemblyKind = enum(c_int) {
vertex,
fragment,
};
pub fn assembleShader(self: *Context, kind: AssemblyKind, name: [*:0]const u8, source: []const u8, injections: anytype) AssembleError![]const u8 {
const frame_allocator = self.frame_arena.allocator();
const injected_source = try glsl.inject(frame_allocator, source, 430, injections);
const result = ext.shaderc_compile_into_spv(self.shader_compiler, injected_source.ptr, injected_source.len, switch (kind) {
.fragment => ext.shaderc_glsl_fragment_shader,
.vertex => ext.shaderc_glsl_vertex_shader,
}, name, "main", self.spirv_options) orelse {
return error.OutOfMemory;
};
defer {
ext.shaderc_result_release(result);
}
return switch (ext.shaderc_result_get_compilation_status(result)) {
ext.shaderc_compilation_status_success => {
const compiled_len = ext.shaderc_result_get_length(result);
const compiled_ptr = ext.shaderc_result_get_bytes(result);
return frame_allocator.dupe(u8, compiled_ptr[0..compiled_len]);
},
ext.shaderc_compilation_status_compilation_error, ext.shaderc_compilation_status_invalid_stage => {
std.log.err("{s}", .{ext.shaderc_result_get_error_message(result)});
std.log.debug("problematic shader:\n{s}", .{injected_source});
return error.BadSyntax;
},
ext.shaderc_compilation_status_internal_error => @panic("shaderc internal compiler error"),
else => unreachable,
pub fn init(context: *const Context) !Commands {
return .{
.pending = try context.shared.acquireQueue(),
};
}
pub fn assembleShaderEmbedded(self: *Context, kind: AssemblyKind, comptime path: [:0]const u8, injections: anytype) AssembleError![]const u8 {
return self.assembleShader(kind, path, @embedFile(path), injections);
}
pub fn deinit(self: *Context) void {
ext.shaderc_compile_options_release(self.spirv_options);
ext.shaderc_compiler_release(self.shader_compiler);
ext.SDL_DestroyGPUDevice(self.gpu_device);
ext.SDL_DestroyWindow(self.window);
self.frame_arena.deinit();
self.* = undefined;
}
pub const traits = ona.Traits{
.is_thread_unsafe = true,
};
};
pub const Display = struct {
@ -87,209 +29,42 @@ pub const Display = struct {
is_hidden: bool,
};
pub const Effect = struct {
pipeline: *ext.SDL_GPUGraphicsPipeline,
};
fn compile_shaders(context: ona.Exclusive(Context), _: ona.Assets(Effect)) !void {
const Camera = extern struct {
projection: [4]@Vector(4, f32),
};
const vertex_spirv = try context.ptr.assembleShaderEmbedded(.vertex, "./gfx/canvas.vert", .{
.model_xy = glsl.nativeInput(0, @Vector(2, f32)),
.model_uv = glsl.nativeInput(1, @Vector(2, f32)),
.model_rgba = glsl.nativeInput(2, @Vector(4, f32)),
.instance_uv_offset = glsl.nativeInput(3, @Vector(2, f32)),
.instance_uv_scale = glsl.nativeInput(4, @Vector(2, f32)),
.instance_xbasis = glsl.nativeInput(5, @Vector(2, f32)),
.instance_ybasis = glsl.nativeInput(6, @Vector(2, f32)),
.instance_origin = glsl.nativeInput(7, @Vector(2, f32)),
.instance_pivot = glsl.nativeInput(8, @Vector(2, f32)),
.instance_rgb = glsl.nativeInput(9, @Vector(3, f32)),
.instance_bits = glsl.nativeInput(10, u32),
.uv = glsl.nativeOutput(0, @Vector(2, f32)),
.rgba = glsl.nativeOutput(1, @Vector(4, f32)),
.camera = glsl.nativeUniformBlock(0, Camera),
});
const fragment_spirv = try context.ptr.assembleShaderEmbedded(.fragment, "./gfx/canvas.frag", .{
.vertex_uv = glsl.nativeInput(0, @Vector(2, f32)),
.vertex_rgba = glsl.nativeInput(1, @Vector(4, f32)),
.source_texture = glsl.Sampler{ .sampler_2d = 0 },
.color = glsl.nativeOutput(0, @Vector(4, f32)),
.camera = glsl.nativeUniformBlock(0, Camera),
});
const effect_vertex = ext.SDL_CreateGPUShader(context.ptr.gpu_device, &.{
.code = vertex_spirv.ptr,
.code_size = vertex_spirv.len,
.format = ext.SDL_GPU_SHADERFORMAT_SPIRV,
.stage = ext.SDL_GPU_SHADERSTAGE_VERTEX,
.entrypoint = "main",
});
defer {
ext.SDL_ReleaseGPUShader(context.ptr.gpu_device, effect_vertex);
}
const effect_fragment = ext.SDL_CreateGPUShader(context.ptr.gpu_device, &.{
.code = fragment_spirv.ptr,
.code_size = fragment_spirv.len,
.format = ext.SDL_GPU_SHADERFORMAT_SPIRV,
.stage = ext.SDL_GPU_SHADERSTAGE_FRAGMENT,
.entrypoint = "main",
});
defer {
ext.SDL_ReleaseGPUShader(context.ptr.gpu_device, effect_fragment);
}
// _ = try effects.insert(.{
// .pipeline = ext.SDL_CreateGPUGraphicsPipeline(context.ptr.gpu_device, &.{
// .vertex_shader = effect_vertex,
// .fragment_shader = effect_fragment,
// .primitive_type = ext.SDL_GPU_PRIMITIVETYPE_TRIANGLELIST,
// }).?,
// });
}
pub fn prepare(display: ona.Write(Display)) void {
display.ptr.is_hidden = false;
}
pub fn render(context: ona.Exclusive(Context)) !void {
const commands = ext.SDL_AcquireGPUCommandBuffer(context.ptr.gpu_device) orelse {
return error.SdlFailure;
};
errdefer {
_ = !ext.SDL_CancelGPUCommandBuffer(commands);
}
var has_swapchain_texture: ?*ext.SDL_GPUTexture = null;
var sawpchain_width, var swapchain_height = [2]u32{ 0, 0 };
if (!ext.SDL_WaitAndAcquireGPUSwapchainTexture(
commands,
context.ptr.window,
&has_swapchain_texture,
&sawpchain_width,
&swapchain_height,
)) {
return error.SdlFailure;
}
if (has_swapchain_texture) |swapchain_texture| {
const color_targets = [_]ext.SDL_GPUColorTargetInfo{
.{
.texture = swapchain_texture,
.load_op = ext.SDL_GPU_LOADOP_CLEAR,
.clear_color = .{ .r = 0, .g = 0.2, .b = 0.4, .a = 1.0 },
},
};
const render_pass = ext.SDL_BeginGPURenderPass(commands, &color_targets, color_targets.len, null) orelse {
return error.SdlFailure;
};
defer {
ext.SDL_EndGPURenderPass(render_pass);
}
}
if (!ext.SDL_SubmitGPUCommandBuffer(commands)) {
return;
}
pub fn queueSynchronize(context: ona.Exclusive(Context), display: ona.Read(Display)) !void {
context.ptr.shared.swapBuffers(display.ptr.*);
}
pub fn setup(app: *ona.App) !void {
if (!ext.SDL_Init(ext.SDL_INIT_VIDEO)) {
if (!c.SDL_Init(c.SDL_INIT_VIDEO)) {
return error.SystemFailure;
}
const display = try app.putState(Display{
const default_display = Display{
.width = 1280,
.height = 720,
.is_hidden = false,
});
const window = ext.SDL_CreateWindow("Ona", display.width, display.height, ext.SDL_WINDOW_HIDDEN) orelse {
return error.SdlFailure;
};
errdefer {
ext.SDL_DestroyWindow(window);
}
try app.setState(default_display);
const gpu_device = ext.SDL_CreateGPUDevice(ext.SDL_GPU_SHADERFORMAT_SPIRV, builtin.mode == .Debug, null) orelse {
return error.SdlFailure;
};
// var shader_compiler = try glsl.Assembly.init(.spirv, switch (builtin.mode) {
// .Debug, .ReleaseSafe => .unoptimized,
// .ReleaseSmall => .optimize_size,
// .ReleaseFast => .optimize_speed,
// });
errdefer {
ext.SDL_DestroyGPUDevice(gpu_device);
}
// errdefer {
// shader_compiler.deinit();
// }
if (!ext.SDL_ClaimWindowForGPUDevice(gpu_device, window)) {
return error.SdlFailure;
}
{
var context = try Context.init(default_display);
const shader_compiler = ext.shaderc_compiler_initialize() orelse {
return error.OutOfMemory;
};
errdefer {
ext.shaderc_compiler_release(shader_compiler);
}
const spirv_options = ext.shaderc_compile_options_initialize() orelse {
return error.OutOfMemory;
};
errdefer {
ext.shaderc_compile_options_release(spirv_options);
}
ext.shaderc_compile_options_set_target_env(spirv_options, ext.shaderc_target_env_vulkan, ext.shaderc_env_version_vulkan_1_1);
ext.shaderc_compile_options_set_optimization_level(spirv_options, switch (builtin.mode) {
.Debug, .ReleaseSafe => ext.shaderc_optimization_level_zero,
.ReleaseSmall => ext.shaderc_optimization_level_size,
.ReleaseFast => ext.shaderc_optimization_level_performance,
});
try ona.registerAsset(app, Effect);
try app.setState(Context{
.frame_arena = .init(coral.heap.allocator),
.shader_compiler = shader_compiler,
.spirv_options = spirv_options,
.window = window,
.gpu_device = gpu_device,
});
try app.on(.load, .of(prepare));
try app.on(.load, .of(compile_shaders));
try app.on(.render, .of(render));
try app.on(.finish, .of(synchronize));
}
pub fn synchronize(context: ona.Exclusive(Context), display: ona.Read(Display)) !void {
const window_flags = ext.SDL_GetWindowFlags(context.ptr.window);
const is_window_hidden = (window_flags & ext.SDL_WINDOW_HIDDEN) != 0;
if (is_window_hidden != display.ptr.is_hidden) {
const was_window_changed = switch (display.ptr.is_hidden) {
true => ext.SDL_HideWindow(context.ptr.window),
false => ext.SDL_ShowWindow(context.ptr.window),
};
if (!was_window_changed) {
std.log.warn("failed to change window visibility", .{});
errdefer {
context.deinit();
}
try app.setState(context);
}
if (!context.ptr.frame_arena.reset(.{ .retain_with_limit = 1024 * 1024 })) {
std.log.warn("failed to shrink frame arena during reset, freeing instead...", .{});
}
try app.on(.render, .of(queueSynchronize));
}

250
src/ona/gfx/Context.zig Normal file
View File

@ -0,0 +1,250 @@
const builtin = @import("builtin");
const c = @cImport({
@cInclude("SDL3/SDL.h");
});
const coral = @import("coral");
const ona = @import("../ona.zig");
const std = @import("std");
shared: *Shared,
thread: std.Thread,
pub const Command = union(enum) {};
pub const Queue = struct {
commands: coral.stack.Sequential(Command) = .empty,
has_next: ?*Queue = null,
pub fn cancel(self: *Queue) void {
self.commands.clear();
}
fn deinit(self: *Queue) void {
self.commands.deinit(coral.heap.allocator);
}
};
const Self = @This();
const Shared = struct {
device: *c.SDL_GPUDevice,
display: ona.gfx.Display,
frame_started: std.Thread.ResetEvent = .{},
frame_finished: std.Thread.ResetEvent = .{},
has_pooled_queue: std.atomic.Value(?*Queue) = .init(null),
swap_buffers: [2]Buffer = .{ .{}, .{} },
is_swapped: bool = false,
is_running: bool = true,
const Buffer = struct {
has_head_queue: ?*Queue = null,
has_tail_queue: ?*Queue = null,
pub fn dequeue(self: *Buffer) ?*Queue {
if (self.has_head_queue) |head| {
self.has_head_queue = head.has_next;
head.has_next = null;
if (self.has_head_queue == null) {
self.has_tail_queue = null;
}
return head;
}
return null;
}
pub fn enqueue(self: *Buffer, queue: *Queue) void {
if (self.has_tail_queue) |tail| {
std.debug.assert(self.has_head_queue != null);
tail.has_next = queue;
self.has_tail_queue = queue;
} else {
self.has_head_queue = queue;
self.has_tail_queue = queue;
}
queue.has_next = null;
}
};
pub fn acquireQueue(self: *Shared) error{OutOfMemory}!*Queue {
const queue = fetch: {
if (self.has_pooled_queue.load(.acquire)) |queue| {
self.has_pooled_queue.store(queue.has_next, .release);
break :fetch queue;
}
const queue = try coral.heap.allocator.create(Queue);
queue.* = .{};
break :fetch queue;
};
self.swap_buffers[@intFromBool(self.is_swapped)].enqueue(queue);
return queue;
}
fn deinit(self: *Shared, thread: std.Thread) void {
self.frame_finished.wait();
self.frame_finished.reset();
self.is_running = false;
self.frame_started.set();
thread.join();
c.SDL_DestroyGPUDevice(self.device);
while (self.has_pooled_queue.load(.acquire)) |queue| {
self.has_pooled_queue.store(queue.has_next, .release);
coral.heap.allocator.destroy(queue);
}
for (&self.swap_buffers) |*buffer| {
while (buffer.dequeue()) |queue| {
queue.deinit();
coral.heap.allocator.destroy(queue);
}
}
}
fn run(self: *Shared) !void {
const window = c.SDL_CreateWindow("Ona", 1280, 720, c.SDL_WINDOW_HIDDEN) orelse {
return error.SdlFailure;
};
defer {
c.SDL_DestroyWindow(window);
}
if (!c.SDL_ClaimWindowForGPUDevice(self.device, window)) {
return error.SdlFailure;
}
while (self.is_running) {
const window_flags = c.SDL_GetWindowFlags(window);
const is_window_hidden = (window_flags & c.SDL_WINDOW_HIDDEN) != 0;
if (is_window_hidden != self.display.is_hidden) {
const was_window_changed = switch (self.display.is_hidden) {
true => c.SDL_HideWindow(window),
false => c.SDL_ShowWindow(window),
};
if (!was_window_changed) {
std.log.warn("failed to change window visibility", .{});
}
}
const commands = c.SDL_AcquireGPUCommandBuffer(self.device) orelse {
return error.SdlFailure;
};
errdefer {
_ = c.SDL_CancelGPUCommandBuffer(commands);
}
var has_swapchain_texture: ?*c.SDL_GPUTexture = null;
var sawpchain_width, var swapchain_height = [2]u32{ 0, 0 };
if (!c.SDL_WaitAndAcquireGPUSwapchainTexture(
commands,
window,
&has_swapchain_texture,
&sawpchain_width,
&swapchain_height,
)) {
return error.SdlFailure;
}
if (has_swapchain_texture) |swapchain_texture| {
const color_targets = [_]c.SDL_GPUColorTargetInfo{
.{
.texture = swapchain_texture,
.load_op = c.SDL_GPU_LOADOP_DONT_CARE,
},
};
const render_pass = c.SDL_BeginGPURenderPass(commands, &color_targets, color_targets.len, null) orelse {
return error.SdlFailure;
};
defer {
c.SDL_EndGPURenderPass(render_pass);
}
while (self.swap_buffers[@intFromBool(!self.is_swapped)].dequeue()) |queue| {
defer {
queue.cancel();
queue.has_next = self.has_pooled_queue.load(.acquire);
self.has_pooled_queue.store(queue, .release);
}
for (queue.commands.items.slice()) |_| {}
}
}
_ = c.SDL_SubmitGPUCommandBuffer(commands);
self.frame_finished.set();
self.frame_started.wait();
self.frame_started.reset();
}
}
pub fn swapBuffers(self: *Shared, display: ona.gfx.Display) void {
self.frame_finished.wait();
self.frame_finished.reset();
self.display = display;
self.is_swapped = !self.is_swapped;
self.frame_started.set();
}
};
pub fn init(display: ona.gfx.Display) !Self {
const device = c.SDL_CreateGPUDevice(c.SDL_GPU_SHADERFORMAT_SPIRV, builtin.mode == .Debug, null) orelse {
return error.SdlFailure;
};
errdefer {
c.SDL_DestroyGPUDevice(device);
}
const shared = try coral.heap.allocator.create(Shared);
errdefer {
coral.heap.allocator.destroy(shared);
}
shared.* = .{
.device = device,
.display = display,
};
shared.frame_started.set();
return .{
.thread = try .spawn(.{}, Shared.run, .{shared}),
.shared = shared,
};
}
pub fn deinit(self: *Self) void {
self.shared.deinit(self.thread);
coral.heap.allocator.destroy(self.shared);
self.* = undefined;
}

View File

@ -0,0 +1,3 @@
void main() {
color = texture(texture, texCoord);
}

View File

@ -0,0 +1,25 @@
const vec2 quadVertices[6] = vec2[](
vec2(-1.0, 1.0),
vec2(-1.0, -1.0),
vec2( 1.0, -1.0),
vec2(-1.0, 1.0),
vec2( 1.0, -1.0),
vec2( 1.0, 1.0)
);
const vec2 quadTexCoords[6] = vec2[](
vec2(0.0, 1.0),
vec2(0.0, 0.0),
vec2(1.0, 0.0),
vec2(0.0, 1.0),
vec2(1.0, 0.0),
vec2(1.0, 1.0)
);
void main() {
gl_Position = vec4(quadVertices[gl_VertexID], 0.0, 1.0);
texCoord = quadTexCoords[gl_VertexID];
}

View File

@ -1,9 +1,140 @@
const c = @cImport({
@cInclude("shaderc/shaderc.h");
});
const coral = @import("coral");
const ona = @import("../ona.zig");
const std = @import("std");
///
/// State machine for compiling GLSL shader source code into targeted executable formats.
///
pub const Assembly = struct {
compiler: c.shaderc_compiler_t,
options: c.shaderc_compile_options_t,
///
/// Optimization preference.
///
pub const Optimizations = enum {
unoptimized,
optimize_speed,
optimize_size,
};
///
/// Target format used for shader binaries.
///
pub const Target = enum {
spirv,
};
///
/// Assembles and allocates the Ona glsl-encoded `source` into a shader binary using `allocator` as the buffer
/// allocation strategy.
///
/// `stage` determines how the compiler attempts to interpret and compile the GLSL, with different stages requiring
/// different source code.
///
/// Failing to compile `source` will result in a errors being logged with what went wrong. It is advised that
/// `name` be supplied a meaningful name to disambiguate which shader the error came from at runtime. In debug
/// builds, a full dump of the problematic shader with the injected code is logged after any error messages.
///
/// Syntax errors will result in `error.BadSyntax` being returned instead.
///
/// **Note** the shader assumes the entry-point of the GLSL program matches the signature `void main()`
///
pub fn assemble(
self: Assembly,
allocator: std.mem.Allocator,
stage: Stage,
name: [*:0]const u8,
source: []const u8,
) (std.mem.Allocator.Error || error{BadSyntax})![]const u8 {
const result = c.shaderc_compile_into_spv(self.shader_compiler, source.ptr, source.len, switch (stage) {
.fragment => c.shaderc_glsl_fragment_shader,
.vertex => c.shaderc_glsl_vertex_shader,
}, name, "main", self.spirv_options) orelse {
return error.OutOfMemory;
};
defer {
c.shaderc_result_release(result);
}
return switch (c.shaderc_result_get_compilation_status(result)) {
c.shaderc_compilation_status_success => {
const compiled_len = c.shaderc_result_get_length(result);
const compiled_ptr = c.shaderc_result_get_bytes(result);
return allocator.dupe(u8, compiled_ptr[0..compiled_len]);
},
c.shaderc_compilation_status_compilation_error, c.shaderc_compilation_status_invalid_stage => {
std.log.err("{s}", .{c.shaderc_result_get_error_message(result)});
std.log.debug("problematic shader:\n{s}", .{source});
return error.BadSyntax;
},
c.shaderc_compilation_status_internal_error => @panic("shaderc internal compiler error"),
else => unreachable,
};
}
pub fn deinit(self: *Assembly) void {
c.shaderc_compile_options_release(self.spirv_options);
c.shaderc_compiler_release(self.shader_compiler);
self.* = undefined;
}
///
/// Returns an `Assembly` with `target` as the desired compilation format and `optimization` as the desired
/// optimization level.
///
/// Should not be enough memory be available, `error.OutOfMemory` is returned instead.
///
/// **Note** optimizations are not guaranteed and will also depend on the `target` specified.
///
pub fn init(target: Target, optimizations: Optimizations) error{OutOfMemory}!Assembly {
const compiler = c.shaderc_compiler_initialize() orelse {
return error.OutOfMemory;
};
errdefer {
c.shaderc_compiler_release(compiler);
}
const options = c.shaderc_compile_options_initialize() orelse {
return error.OutOfMemory;
};
errdefer {
c.shaderc_compile_options_release(options);
}
switch (target) {
.spirv => {
c.shaderc_compile_options_set_target_env(options, c.shaderc_target_env_vulkan, c.shaderc_env_version_vulkan_1_1);
},
}
c.shaderc_compile_options_set_optimization_level(options, switch (optimizations) {
.unoptimized => c.shaderc_optimization_level_zero,
.optimize_size => c.shaderc_optimization_level_size,
.optimize_speed => c.shaderc_optimization_level_performance,
});
return .{
.compiler = compiler,
.options = options,
};
}
};
pub const Field = struct {
name: [:0]const u8,
primitive: Primitive,
@ -81,6 +212,11 @@ pub const Sampler = union(enum) {
}
};
pub const Stage = enum {
vertex,
fragment,
};
pub const UniformBlock = struct {
name: [:0]const u8,
binding: usize,

View File

@ -1,4 +1,4 @@
const ext = @cImport({
const c = @cImport({
@cInclude("SDL3/SDL.h");
});
@ -201,22 +201,22 @@ pub const KeyScancode = enum(u32) {
};
pub fn poll(exit: ona.Send(ona.App.Exit), hid_events: ona.Send(ona.hid.Event)) !void {
var event: ext.SDL_Event = undefined;
var event: c.SDL_Event = undefined;
while (ext.SDL_PollEvent(&event)) {
while (c.SDL_PollEvent(&event)) {
try hid_events.push(switch (event.type) {
ext.SDL_EVENT_QUIT => {
c.SDL_EVENT_QUIT => {
return exit.push(.success);
},
ext.SDL_EVENT_KEY_UP => .{ .key_up = @enumFromInt(event.key.scancode) },
ext.SDL_EVENT_KEY_DOWN => .{ .key_down = @enumFromInt(event.key.scancode) },
c.SDL_EVENT_KEY_UP => .{ .key_up = @enumFromInt(event.key.scancode) },
c.SDL_EVENT_KEY_DOWN => .{ .key_down = @enumFromInt(event.key.scancode) },
ext.SDL_EVENT_MOUSE_BUTTON_UP => .{
c.SDL_EVENT_MOUSE_BUTTON_UP => .{
.mouse_up = switch (event.button.button) {
ext.SDL_BUTTON_LEFT => .left,
ext.SDL_BUTTON_RIGHT => .right,
ext.SDL_BUTTON_MIDDLE => .middle,
c.SDL_BUTTON_LEFT => .left,
c.SDL_BUTTON_RIGHT => .right,
c.SDL_BUTTON_MIDDLE => .middle,
else => {
continue;
@ -224,11 +224,11 @@ pub fn poll(exit: ona.Send(ona.App.Exit), hid_events: ona.Send(ona.hid.Event)) !
},
},
ext.SDL_EVENT_MOUSE_BUTTON_DOWN => .{
c.SDL_EVENT_MOUSE_BUTTON_DOWN => .{
.mouse_down = switch (event.button.button) {
ext.SDL_BUTTON_LEFT => .left,
ext.SDL_BUTTON_RIGHT => .right,
ext.SDL_BUTTON_MIDDLE => .middle,
c.SDL_BUTTON_LEFT => .left,
c.SDL_BUTTON_RIGHT => .right,
c.SDL_BUTTON_MIDDLE => .middle,
else => {
continue;
@ -236,7 +236,7 @@ pub fn poll(exit: ona.Send(ona.App.Exit), hid_events: ona.Send(ona.hid.Event)) !
},
},
ext.SDL_EVENT_MOUSE_MOTION => .{
c.SDL_EVENT_MOUSE_MOTION => .{
.mouse_motion = .{
.relative_position = .{ event.motion.xrel, event.motion.yrel },
.absolute_position = .{ event.motion.x, event.motion.y },
@ -251,6 +251,10 @@ pub fn poll(exit: ona.Send(ona.App.Exit), hid_events: ona.Send(ona.hid.Event)) !
}
pub fn setup(app: *ona.App) !void {
if (!c.SDL_Init(c.SDL_INIT_EVENTS)) {
return error.SystemFailure;
}
try app.on(.post_update, .of(poll));
try ona.registerChannel(app, Event);
}

View File

@ -296,7 +296,6 @@ fn run_realtime_loop(app: *App) !void {
try app.run(&tasks, .post_update);
try app.run(&tasks, .render);
try app.run(&tasks, .finish);
const exit_messages = exit_channel.messages();