diff --git a/debug/ca.frag b/debug/ca.frag new file mode 100644 index 0000000..b4218e0 --- /dev/null +++ b/debug/ca.frag @@ -0,0 +1,28 @@ +#version 430 + +layout (binding = 0) uniform sampler2D sprite; + +layout (location = 0) in vec4 color; +layout (location = 1) in vec2 uv; + +layout (location = 0) out vec4 texel; + +layout (binding = 0) readonly buffer Camera { + mat4 projection_matrix; +}; + +layout (binding = 1) readonly buffer Material { + float effect_magnitude; +}; + +void main() { + const vec2 red_channel_uv = uv + vec2(effect_magnitude, 0.0); + const vec2 blue_channel_uv = uv + vec2(-effect_magnitude, 0.0); + const vec4 original_texel = texture(sprite, uv); + + texel = vec4(texture(sprite, red_channel_uv).r, original_texel.g, texture(sprite, blue_channel_uv).b, original_texel.a) * color; + + if (texel.a == 0) { + discard; + } +} diff --git a/debug/test.bmp b/debug/test.bmp deleted file mode 100644 index 2d7f6e7..0000000 --- a/debug/test.bmp +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e31f120e1c23ea9c949441bca87e504c1e8f2e90b6dff9bd1cd559ddfd4dd225 -size 16522 diff --git a/src/coral/ascii.zig b/src/coral/ascii.zig index 0339e7d..11830ee 100644 --- a/src/coral/ascii.zig +++ b/src/coral/ascii.zig @@ -7,7 +7,7 @@ const scalars = @import("./scalars.zig"); const std = @import("std"); pub const DecimalFormat = struct { - delimiter: []const coral.Byte, + delimiter: []const coral.io.Byte, positive_prefix: enum {none, plus, space}, pub const default = DecimalFormat{ @@ -115,9 +115,9 @@ pub const DecimalFormat = struct { } } - pub fn format(self: DecimalFormat, writer: io.Writer, value: anytype) io.PrintError!void { + pub fn format(self: DecimalFormat, writer: io.Writer, value: anytype) io.Error!void { if (value == 0) { - return io.print(writer, switch (self.positive_prefix) { + return io.write_all(writer, switch (self.positive_prefix) { .none => "0", .plus => "+0", .space => " 0", @@ -151,21 +151,21 @@ pub const DecimalFormat = struct { } } - return io.print(writer, buffer[buffer_start ..]); + return io.write_all(writer, buffer[buffer_start ..]); }, .Float => |float| { if (value < 0) { - try io.print(writer, "-"); + try io.write_all(writer, "-"); } const Float = @TypeOf(value); const Int = std.meta.Int(.unsigned, float.bits); const integer = @as(Int, @intFromFloat(value)); - try self.print(writer, integer); - try io.print(writer, "."); - try self.print(writer, @as(Int, @intFromFloat((value - @as(Float, @floatFromInt(integer))) * 100))); + try self.format(writer, integer); + try io.write_all(writer, "."); + try self.format(writer, @as(Int, @intFromFloat((value - @as(Float, @floatFromInt(integer))) * 100))); }, else => @compileError("`" ++ @typeName(Value) ++ "` cannot be formatted to a decimal string"), diff --git a/src/coral/asyncio.zig b/src/coral/asyncio.zig new file mode 100644 index 0000000..485beab --- /dev/null +++ b/src/coral/asyncio.zig @@ -0,0 +1,76 @@ +const std = @import("std"); + +pub fn BlockingQueue(comptime max: usize, comptime T: type) type { + return struct { + buffer: [max]T = undefined, + head: usize = 0, + tail: usize = 0, + mutex: std.Thread.Mutex = .{}, + not_empty: std.Thread.Condition = .{}, + not_full: std.Thread.Condition = .{}, + + const Self = @This(); + + pub fn enqueue(self: *Self, item: T) void { + self.mutex.lock(); + + defer self.mutex.unlock(); + + while (((self.tail + 1) % max) == self.head) { + self.not_full.wait(&self.mutex); + } + + self.buffer[self.tail] = item; + self.tail = (self.tail + 1) % max; + + self.not_empty.signal(); + } + + pub fn dequeue(self: *Self) T { + self.mutex.lock(); + + defer self.mutex.unlock(); + + while (self.head == self.tail) { + self.not_empty.wait(&self.mutex); + } + + const item = self.buffer[self.head]; + + self.buffer[self.head] = undefined; + self.head = (self.head + 1) % max; + + self.not_full.signal(); + + return item; + } + }; +} + +pub fn Future(comptime T: type) type { + return struct { + value: ?T = null, + resolved: std.Thread.ResetEvent = .{}, + + const Self = @This(); + + pub fn get(self: *Self) *T { + // TODO: Make async. + self.resolved.wait(); + + return &self.value.?; + } + + pub fn resolve(self: *Self, value: T) bool { + if (self.resolved.isSet()) { + return false; + } + + self.value = value; + + self.resolved.set(); + + return true; + } + }; +} diff --git a/src/coral/coral.zig b/src/coral/coral.zig index fae4ce8..777aca3 100644 --- a/src/coral/coral.zig +++ b/src/coral/coral.zig @@ -1,5 +1,7 @@ pub const ascii = @import("./ascii.zig"); +pub const asyncio = @import("./asyncio.zig"); + pub const files = @import("./files.zig"); pub const hashes = @import("./hashes.zig"); @@ -16,6 +18,108 @@ pub const slices = @import("./slices.zig"); pub const stack = @import("./stack.zig"); +const std = @import("std"); + pub const utf8 = @import("./utf8.zig"); pub const vectors = @import("./vectors.zig"); + +pub fn Pool(comptime Value: type) type { + return struct { + entries: stack.Sequential(Entry), + first_free_index: usize = 0, + + const Entry = union (enum) { + free_index: usize, + occupied: Value, + }; + + pub const Values = struct { + cursor: usize = 0, + pool: *const Self, + + pub fn next(self: *Values) ?*Value { + while (self.cursor < self.pool.entries.len()) { + defer self.cursor += 1; + + switch (self.pool.entries.values[self.cursor]) { + .free_index => { + continue; + }, + + .occupied => |*occupied| { + return occupied; + }, + } + } + + return null; + } + }; + + const Self = @This(); + + pub fn deinit(self: *Self) void { + self.entries.deinit(); + + self.* = undefined; + } + + pub fn get(self: *Self, key: usize) ?*Value { + return switch (self.entries.values[key]) { + .free_index => null, + .occupied => |*occupied| occupied, + }; + } + + pub fn init(allocator: std.mem.Allocator) Self { + return .{ + .entries = .{.allocator = allocator}, + }; + } + + pub fn insert(self: *Self, value: Value) std.mem.Allocator.Error!usize { + const entries_count = self.entries.len(); + + if (self.first_free_index == entries_count) { + try self.entries.push_grow(.{.occupied = value}); + + self.first_free_index += 1; + + return entries_count; + } + + const insersion_index = self.first_free_index; + + self.first_free_index = self.entries.values[self.first_free_index].free_index; + self.entries.values[insersion_index] = .{.occupied = value}; + + return insersion_index; + } + + pub fn remove(self: *Self, key: usize) ?Value { + if (key >= self.entries.len()) { + return null; + } + + switch (self.entries.values[key]) { + .free_index => { + return null; + }, + + .occupied => |occupied| { + self.entries.values[key] = .{.free_index = self.first_free_index}; + self.first_free_index = key; + + return occupied; + }, + } + } + + pub fn values(self: *const Self) Values { + return .{ + .pool = self, + }; + } + }; +} diff --git a/src/coral/files.zig b/src/coral/files.zig index 273202f..8756f3e 100644 --- a/src/coral/files.zig +++ b/src/coral/files.zig @@ -4,11 +4,20 @@ const io = @import("./io.zig"); const std = @import("std"); -pub const Error = error { +pub const AccessError = error { FileNotFound, FileInaccessible, }; +pub const ReadAllError = AccessError || error { + ReadIncomplete, +}; + +pub const ReadAllOptions = struct { + offset: u64 = 0, + limit: u64 = std.math.maxInt(u64), +}; + pub const Stat = struct { size: u64, }; @@ -18,15 +27,37 @@ pub const Storage = struct { vtable: *const VTable, pub const VTable = struct { - stat: *const fn (*anyopaque, []const u8) Error!Stat, - read: *const fn (*anyopaque, []const u8, usize, []io.Byte) Error!usize, + stat: *const fn (*anyopaque, []const u8) AccessError!Stat, + read: *const fn (*anyopaque, []const u8, usize, []io.Byte) AccessError!usize, }; - pub fn read_bytes(self: Storage, path: []const u8, offset: usize, output: []io.Byte) Error!usize { + pub fn read(self: Storage, path: []const u8, output: []io.Byte, offset: u64) AccessError!usize { return self.vtable.read(self.userdata, path, offset, output); } - pub fn read_foreign(self: Storage, path: []const u8, offset: usize, comptime Type: type) Error!?Type { + pub fn read_all(self: Storage, path: []const u8, output: []io.Byte, options: ReadAllOptions) ReadAllError![]const io.Byte { + const bytes_read = try self.vtable.read(self.userdata, path, options.offset, output); + + if (try self.vtable.read(self.userdata, path, options.offset, output) != output.len) { + return error.ReadIncomplete; + } + + return output[0 .. bytes_read]; + } + + pub fn read_alloc(self: Storage, path: []const u8, allocator: std.mem.Allocator, options: ReadAllOptions) (std.mem.Allocator.Error || ReadAllError)![]io.Byte { + const buffer = try allocator.alloc(io.Byte, @min((try self.stat(path)).size, options.limit)); + + errdefer allocator.free(buffer); + + if (try self.vtable.read(self.userdata, path, options.offset, buffer) != buffer.len) { + return error.ReadIncomplete; + } + + return buffer; + } + + pub fn read_foreign(self: Storage, path: []const u8, offset: u64, comptime Type: type) AccessError!?Type { const decoded = (try self.read_native(path, offset, Type)) orelse { return null; }; @@ -37,7 +68,7 @@ pub const Storage = struct { }; } - pub fn read_native(self: Storage, path: []const u8, offset: usize, comptime Type: type) Error!?Type { + pub fn read_native(self: Storage, path: []const u8, offset: u64, comptime Type: type) AccessError!?Type { var buffer = @as([@sizeOf(Type)]io.Byte, undefined); if (try self.vtable.read(self.userdata, path, offset, &buffer) != buffer.len) { @@ -47,6 +78,10 @@ pub const Storage = struct { return @as(*align(1) const Type, @ptrCast(&buffer)).*; } + pub fn stat(self: Storage, path: []const u8) AccessError!Stat { + return self.vtable.stat(self.userdata, path); + } + pub const read_little = switch (native_endian) { .little => read_native, .big => read_foreign, @@ -60,7 +95,7 @@ pub const Storage = struct { pub const bundle = init: { const Bundle = struct { - fn full_path(path: []const u8) Error![4095:0]u8 { + fn full_path(path: []const u8) AccessError![4095:0]u8 { var buffer = [_:0]u8{0} ** 4095; _ = std.fs.cwd().realpath(path, &buffer) catch { @@ -70,7 +105,7 @@ pub const bundle = init: { return buffer; } - fn read(_: *anyopaque, path: []const u8, offset: usize, output: []io.Byte) Error!usize { + fn read(_: *anyopaque, path: []const u8, offset: usize, output: []io.Byte) AccessError!usize { var file = std.fs.openFileAbsoluteZ(&(try full_path(path)), .{.mode = .read_only}) catch |open_error| { return switch (open_error) { error.FileNotFound => error.FileNotFound, @@ -89,7 +124,7 @@ pub const bundle = init: { return file.read(output) catch error.FileInaccessible; } - fn stat(_: *anyopaque, path: []const u8) Error!Stat { + fn stat(_: *anyopaque, path: []const u8) AccessError!Stat { const file_stat = get: { var file = std.fs.openFileAbsoluteZ(&(try full_path(path)), .{.mode = .read_only}) catch |open_error| { return switch (open_error) { diff --git a/src/coral/io.zig b/src/coral/io.zig index 545b64f..e220a55 100644 --- a/src/coral/io.zig +++ b/src/coral/io.zig @@ -138,16 +138,6 @@ pub fn alloc_read(input: coral.io.Reader, allocator: std.mem.Allocator) []coral. pub const bits_per_byte = 8; -pub fn bytes_of(value: anytype) []const Byte { - const pointer_info = @typeInfo(@TypeOf(value)).Pointer; - - return switch (pointer_info.size) { - .One => @as([*]const Byte, @ptrCast(value))[0 .. @sizeOf(pointer_info.child)], - .Slice => @as([*]const Byte, @ptrCast(value.ptr))[0 .. @sizeOf(pointer_info.child) * value.len], - else => @compileError("`value` must be single-element pointer or slice type"), - }; -} - pub fn skip_n(input: Reader, distance: u64) Error!void { var buffer = @as([512]coral.Byte, undefined); var remaining = distance; @@ -163,16 +153,6 @@ pub fn skip_n(input: Reader, distance: u64) Error!void { } } -pub fn slice_sentineled(comptime Sentinel: type, comptime sen: Sentinel, ptr: [*:sen]const Sentinel) [:sen]const Sentinel { - var len = @as(usize, 0); - - while (ptr[len] != sen) { - len += 1; - } - - return ptr[0 .. len:sen]; -} - pub fn stream_all(input: Reader, output: Writer) Error!usize { var buffer = @as([512]coral.Byte, undefined); var copied = @as(usize, 0); diff --git a/src/coral/map.zig b/src/coral/map.zig index 6ac848e..eb7de4d 100644 --- a/src/coral/map.zig +++ b/src/coral/map.zig @@ -147,7 +147,7 @@ pub fn Hashed(comptime Key: type, comptime Value: type, comptime traits: Traits( self.* = undefined; } - pub fn get_ptr(self: Self, key: Key) ?*Value { + pub fn get(self: Self, key: Key) ?*Value { if (self.len == 0) { return null; } @@ -169,14 +169,6 @@ pub fn Hashed(comptime Key: type, comptime Value: type, comptime traits: Traits( return null; } - pub fn get(self: Self, key: Key) ?Value { - if (self.get_ptr(key)) |value| { - return value.*; - } - - return null; - } - pub fn emplace(self: *Self, key: Key, value: Value) std.mem.Allocator.Error!bool { try self.rehash(load_max); diff --git a/src/coral/slices.zig b/src/coral/slices.zig index 5a0953f..2a4f80a 100644 --- a/src/coral/slices.zig +++ b/src/coral/slices.zig @@ -115,24 +115,6 @@ pub fn get_ptr(slice: anytype, index: usize) ?ElementPtr(@TypeOf(slice)) { return &slice[index]; } -pub fn index_of(haystack: anytype, offset: usize, needle: std.meta.Child(@TypeOf(haystack))) ?usize { - for (offset .. haystack.len) |i| { - if (haystack[i] == needle) { - return i; - } - } - - return null; -} - -pub fn index_of_any(haystack: anytype, offset: usize, needle: []const std.meta.Child(@TypeOf(haystack))) ?usize { - return std.mem.indexOfAnyPos(std.meta.Child(@TypeOf(haystack)), haystack, offset, needle); -} - -pub fn index_of_seq(haystack: anytype, offset: usize, needle: []const std.meta.Child(@TypeOf(haystack))) ?usize { - return std.mem.indexOfPos(std.meta.Child(@TypeOf(haystack)), haystack, offset, needle); -} - pub fn parallel_alloc(comptime Element: type, allocator: std.mem.Allocator, n: usize) std.mem.Allocator.Error!Parallel(Element) { const alignment = @alignOf(Element); const Slices = Parallel(Element); diff --git a/src/coral/utf8.zig b/src/coral/utf8.zig index 4992107..44000e7 100644 --- a/src/coral/utf8.zig +++ b/src/coral/utf8.zig @@ -6,17 +6,14 @@ const io = @import("./io.zig"); const std = @import("std"); -pub fn alloc_formatted(allocator: std.mem.Allocator, comptime format: []const u8, args: anytype) std.mem.Allocator.Error![]coral.Byte { - var buffer = coral.Stack(coral.Byte){.allocator = allocator}; +pub fn alloc_formatted(allocator: std.mem.Allocator, comptime format: []const u8, args: anytype) std.mem.Allocator.Error![:0]u8 { const formatted_len = count_formatted(format, args); + const buffer = try allocator.allocSentinel(u8, formatted_len, 0); - try buffer.grow(formatted_len); + errdefer allocator.free(buffer); - errdefer buffer.deinit(); - - print_formatted(buffer.writer(), format, args) catch unreachable; - - return buffer.to_allocation(formatted_len, 0); + // TODO: This is dumb. + return @constCast(print_formatted(buffer, format, args) catch unreachable); } pub fn count_formatted(comptime format: []const u8, args: anytype) usize { @@ -27,7 +24,7 @@ pub fn count_formatted(comptime format: []const u8, args: anytype) usize { return count.written; } -pub fn print_formatted(buffer: [:0]coral.io.Byte, comptime format: []const u8, args: anytype) io.Error![:0]u8 { +pub fn print_formatted(buffer: [:0]coral.io.Byte, comptime format: []const u8, args: anytype) io.Error![:0]const u8 { const Seekable = struct { buffer: []coral.io.Byte, cursor: usize, @@ -143,8 +140,8 @@ noinline fn print_formatted_value(writer: io.Writer, value: anytype) io.Error!vo const Value = @TypeOf(value); return switch (@typeInfo(Value)) { - .Int => ascii.DecimalFormat.default.print(writer, value), - .Float => ascii.DecimalFormat.default.print(writer, value), + .Int => ascii.DecimalFormat.default.format(writer, value), + .Float => ascii.DecimalFormat.default.format(writer, value), .Enum => io.print(writer, @tagName(value)), .Pointer => |pointer| switch (pointer.size) { diff --git a/src/flow/dag.zig b/src/flow/dag.zig index ea35bc0..931a49f 100644 --- a/src/flow/dag.zig +++ b/src/flow/dag.zig @@ -80,7 +80,7 @@ pub fn Graph(comptime Payload: type) type { return false; }; - if (coral.slices.index_of(edges.values, 0, edge_node) == null) { + if (std.mem.indexOfScalar(Node, edges.values, edge_node) == null) { try edges.push_grow(edge_node); } diff --git a/src/flow/states.zig b/src/flow/states.zig index d4a0e2e..d5874d5 100644 --- a/src/flow/states.zig +++ b/src/flow/states.zig @@ -18,7 +18,7 @@ pub const Table = struct { } pub fn get(self: Table, comptime Resource: type) ?*Resource { - if (self.table.get_ptr(type_id(Resource))) |entry| { + if (self.table.get(type_id(Resource))) |entry| { return @ptrCast(@alignCast(entry.ptr)); } @@ -42,7 +42,7 @@ pub const Table = struct { const Value = @TypeOf(value); const value_id = type_id(Value); - if (self.table.get_ptr(value_id)) |entry| { + if (self.table.get(value_id)) |entry| { @as(*Value, @ptrCast(@alignCast(entry.ptr))).* = value; } else { const resource_allocator = self.arena.allocator(); diff --git a/src/flow/system.zig b/src/flow/system.zig index 25dce4d..01e4a17 100644 --- a/src/flow/system.zig +++ b/src/flow/system.zig @@ -49,16 +49,16 @@ pub const BindContext = struct { } const read_write_resource_nodes = lazily_create: { - break: lazily_create self.systems.read_write_resource_id_nodes.get_ptr(id) orelse insert: { + break: lazily_create self.systems.read_write_resource_id_nodes.get(id) orelse insert: { std.debug.assert(try self.systems.read_write_resource_id_nodes.emplace(id, .{ .allocator = coral.heap.allocator, })); - break: insert self.systems.read_write_resource_id_nodes.get_ptr(id).?; + break: insert self.systems.read_write_resource_id_nodes.get(id).?; }; }; - if (coral.slices.index_of(read_write_resource_nodes.values, 0, self.node) == null) { + if (std.mem.indexOfScalar(dag.Node, read_write_resource_nodes.values, self.node) == null) { try read_write_resource_nodes.push_grow(self.node); } @@ -77,16 +77,16 @@ pub const BindContext = struct { } const read_only_resource_nodes = lazily_create: { - break: lazily_create self.systems.read_only_resource_id_nodes.get_ptr(id) orelse insert: { + break: lazily_create self.systems.read_only_resource_id_nodes.get(id) orelse insert: { std.debug.assert(try self.systems.read_only_resource_id_nodes.emplace(id, .{ .allocator = coral.heap.allocator, })); - break: insert self.systems.read_only_resource_id_nodes.get_ptr(id).?; + break: insert self.systems.read_only_resource_id_nodes.get(id).?; }; }; - if (coral.slices.index_of(read_only_resource_nodes.values, 0, self.node) == null) { + if (std.mem.indexOfScalar(dag.Node, read_only_resource_nodes.values, self.node) == null) { try read_only_resource_nodes.push_grow(self.node); } @@ -415,12 +415,12 @@ pub const Schedule = struct { const nodes = lazily_create: { const system_id = @intFromPtr(info); - break: lazily_create self.system_id_nodes.get_ptr(system_id) orelse insert: { + break: lazily_create self.system_id_nodes.get(system_id) orelse insert: { std.debug.assert(try self.system_id_nodes.emplace(system_id, .{ .allocator = self.system_id_nodes.allocator, })); - break: insert self.system_id_nodes.get_ptr(system_id).?; + break: insert self.system_id_nodes.get(system_id).?; }; }; diff --git a/src/main.zig b/src/main.zig index b49021c..f345239 100644 --- a/src/main.zig +++ b/src/main.zig @@ -5,10 +5,9 @@ const std = @import("std"); const ona = @import("ona"); const Actors = struct { - instances: coral.stack.Sequential(ona.gfx.Point2D) = .{.allocator = coral.heap.allocator}, - quad_mesh_2d: ?*ona.gfx.Handle = null, - body_texture: ?*ona.gfx.Handle = null, - render_texture: ?*ona.gfx.Handle = null, + instances: coral.stack.Sequential(@Vector(2, f32)) = .{.allocator = coral.heap.allocator}, + body_texture: ona.gfx.Texture = .default, + render_texture: ona.gfx.Texture = .default, }; const Player = struct { @@ -23,20 +22,17 @@ pub fn main() !void { }); } -fn load(display: ona.Write(ona.gfx.Display), actors: ona.Write(Actors), assets: ona.Write(ona.gfx.Assets)) !void { - display.res.width, display.res.height = .{1280, 720}; - actors.res.body_texture = try assets.res.create_from_file(coral.files.bundle, "actor.bmp"); - actors.res.quad_mesh_2d = try assets.res.create_quad_mesh_2d(@splat(1)); +fn load(config: ona.Write(ona.gfx.Config), actors: ona.Write(Actors), assets: ona.Write(ona.gfx.Assets)) !void { + config.res.width, config.res.height = .{1280, 720}; + actors.res.body_texture = try assets.res.load_texture_file(coral.files.bundle, "actor.bmp"); - actors.res.render_texture = try assets.res.context.create(.{ - .texture = .{ - .format = .rgba8, + actors.res.render_texture = try assets.res.load_texture(.{ + .format = .rgba8, - .access = .{ - .render = .{ - .width = display.res.width, - .height = display.res.height, - }, + .access = .{ + .render = .{ + .width = config.res.width, + .height = config.res.height, }, }, }); @@ -48,41 +44,41 @@ fn exit(actors: ona.Write(Actors)) void { actors.res.instances.deinit(); } -fn render(queue: ona.gfx.Queue, actors: ona.Write(Actors), display: ona.Read(ona.gfx.Display)) !void { - try queue.commands.append(.{.target = .{ - .texture = actors.res.render_texture.?, +fn render(commands: ona.gfx.Commands, actors: ona.Write(Actors), config: ona.Read(ona.gfx.Config)) !void { + try commands.set_target(.{ + .texture = actors.res.render_texture, .clear_color = .{0, 0, 0, 0}, .clear_depth = 0, - }}); + .clear_stencil = 0, + }); for (actors.res.instances.values) |instance| { - try queue.commands.append(.{.instance_2d = .{ - .mesh_2d = actors.res.quad_mesh_2d.?, - .texture = actors.res.body_texture.?, + try commands.draw_texture(.{ + .texture = actors.res.body_texture, .transform = .{ .origin = instance, .xbasis = .{64, 0}, .ybasis = .{0, 64}, }, - }}); + }); } - try queue.commands.append(.{.target = .{ + try commands.set_target(.{ .clear_color = null, .clear_depth = null, - }}); + .clear_stencil = null, + }); - try queue.commands.append(.{.instance_2d = .{ - .mesh_2d = actors.res.quad_mesh_2d.?, - .texture = actors.res.render_texture.?, + try commands.draw_texture(.{ + .texture = actors.res.render_texture, .transform = .{ - .origin = .{@floatFromInt(display.res.width / 2), @floatFromInt(display.res.height / 2)}, - .xbasis = .{@floatFromInt(display.res.width), 0}, - .ybasis = .{0, @floatFromInt(display.res.height)}, + .origin = .{@floatFromInt(config.res.width / 2), @floatFromInt(config.res.height / 2)}, + .xbasis = .{@floatFromInt(config.res.width), 0}, + .ybasis = .{0, @floatFromInt(config.res.height)}, }, - }}); + }); } fn update(player: ona.Read(Player), actors: ona.Write(Actors), mapping: ona.Read(ona.act.Mapping)) !void { diff --git a/src/ona/gfx.zig b/src/ona/gfx.zig index 912b24d..8a6780f 100644 --- a/src/ona/gfx.zig +++ b/src/ona/gfx.zig @@ -1,40 +1,89 @@ const App = @import("./App.zig"); +pub const colors = @import("./gfx/colors.zig"); + const coral = @import("coral"); -const device = @import("./gfx/device.zig"); +const bmp = @import("./gfx/bmp.zig"); const ext = @import("./ext.zig"); const flow = @import("flow"); -const formats = @import("./gfx/formats.zig"); +const handles = @import("./gfx/handles.zig"); + +pub const lina = @import("./gfx/lina.zig"); const msg = @import("./msg.zig"); +const spirv = @import("./gfx/spirv.zig"); + +const rendering = @import("./gfx/rendering.zig"); + const std = @import("std"); -pub const AssetFormat = struct { - extension: []const u8, - file_desc: *const FileDesc, +pub const Assets = struct { + window: *ext.SDL_Window, + texture_formats: coral.stack.Sequential(TextureFormat), + staging_arena: std.heap.ArenaAllocator, + frame_rendered: std.Thread.ResetEvent = .{}, - pub const Error = std.mem.Allocator.Error || coral.files.Error || error { + pub const LoadError = std.mem.Allocator.Error; + + pub const LoadFileError = LoadError || coral.files.AccessError || error { FormatUnsupported, }; - pub const FileDesc = fn (*std.heap.ArenaAllocator, coral.files.Storage, []const u8) Error!Desc; -}; + pub const TextureFormat = struct { + extension: []const u8, + load_file: *const fn (*std.heap.ArenaAllocator, coral.files.Storage, []const u8) LoadFileError!Texture.Desc, + }; -pub const Assets = struct { - context: device.Context, - formats: coral.stack.Sequential(AssetFormat), - staging_arena: std.heap.ArenaAllocator, + fn deinit(self: *Assets) void { + rendering.enqueue_work(.shutdown); + self.staging_arena.deinit(); + self.texture_formats.deinit(); + } - pub fn create_from_file( - self: *Assets, - storage: coral.files.Storage, - path: []const u8, - ) (OpenError || AssetFormat.Error)!*Handle { + fn init() !Assets { + const window = create: { + const position = ext.SDL_WINDOWPOS_CENTERED; + const flags = ext.SDL_WINDOW_OPENGL; + const width = 640; + const height = 480; + + break: create ext.SDL_CreateWindow("Ona", position, position, width, height, flags) orelse { + return error.Unsupported; + }; + }; + + errdefer { + ext.SDL_DestroyWindow(window); + } + + try rendering.startup(window); + + return .{ + .staging_arena = std.heap.ArenaAllocator.init(coral.heap.allocator), + .texture_formats = .{.allocator = coral.heap.allocator}, + .window = window, + }; + } + + pub fn load_texture(_: *Assets, desc: Texture.Desc) std.mem.Allocator.Error!Texture { + var loaded = coral.asyncio.Future(std.mem.Allocator.Error!Texture){}; + + rendering.enqueue_work(.{ + .load_texture = .{ + .desc = desc, + .loaded = &loaded, + }, + }); + + return loaded.get().*; + } + + pub fn load_texture_file(self: *Assets, storage: coral.files.Storage, path: []const u8) LoadFileError!Texture { defer { const max_cache_size = 536870912; @@ -43,107 +92,62 @@ pub const Assets = struct { } } - for (self.formats.values) |format| { + for (self.texture_formats.values) |format| { if (!std.mem.endsWith(u8, path, format.extension)) { continue; } - return self.context.create(try format.file_desc(&self.staging_arena, storage, path)); + return self.load_texture(try format.load_file(&self.staging_arena, storage, path)); } return error.FormatUnsupported; } - pub fn create_quad_mesh_2d(self: *Assets, extents: Point2D) OpenError!*Handle { - const width, const height = extents / @as(Point2D, @splat(2)); - - return self.context.create(.{ - .mesh_2d = .{ - .indices = &.{0, 1, 2, 0, 2, 3}, - - .vertices = &.{ - .{.xy = .{-width, -height}, .uv = .{0, 1}}, - .{.xy = .{width, -height}, .uv = .{1, 1}}, - .{.xy = .{width, height}, .uv = .{1, 0}}, - .{.xy = .{-width, height}, .uv = .{0, 0}}, - }, - }, - }); - } - pub const thread_restriction = .main; }; -pub const Color = @Vector(4, f32); +pub const Commands = struct { + list: *rendering.Commands.List, -pub const Command = union (enum) { - instance_2d: Instance2D, - target: Target, - - pub const Instance2D = struct { - texture: *Handle, - mesh_2d: *Handle, - transform: Transform2D, + pub const State = struct { + commands: *rendering.Commands, }; - pub const Target = struct { - texture: ?*Handle = null, - clear_color: ?Color, - clear_depth: ?f32, - }; + pub fn bind(_: flow.system.BindContext) std.mem.Allocator.Error!State { + var created_commands = coral.asyncio.Future(std.mem.Allocator.Error!*rendering.Commands){}; + + rendering.enqueue_work(.{ + .create_commands = .{ + .created = &created_commands, + }, + }); + + return .{ + .commands = try created_commands.get().*, + }; + } + + pub fn init(state: *State) Commands { + return .{ + .list = state.commands.pending_list(), + }; + } + + pub fn draw_texture(self: Commands, command: rendering.Command.DrawTexture) std.mem.Allocator.Error!void { + try self.list.append(.{.draw_texture = command}); + } + + pub fn set_target(self: Commands, command: rendering.Command.SetTarget) std.mem.Allocator.Error!void { + try self.list.append(.{.set_target = command}); + } }; -pub const Desc = union (enum) { - texture: Texture, - mesh_2d: Mesh2D, - - pub const Mesh2D = struct { - vertices: []const Vertex, - indices: []const u16, - - pub const Vertex = struct { - xy: Point2D, - uv: Point2D, - }; - }; - - pub const Texture = struct { - format: Format, - access: Access, - - pub const Access = union (enum) { - static: struct { - width: u16, - data: []const coral.io.Byte, - }, - - render: struct { - width: u16, - height: u16, - }, - }; - - pub const Format = enum { - rgba8, - bgra8, - - pub fn byte_size(self: Format) usize { - return switch (self) { - .rgba8, .bgra8 => 4, - }; - } - }; - }; -}; - -pub const Display = struct { +pub const Config = struct { width: u16 = 1280, height: u16 = 720, - clear_color: Color = colors.black, + clear_color: lina.Color = colors.black, }; -pub const Handle = opaque {}; - pub const Input = union (enum) { key_up: Key, key_down: Key, @@ -160,57 +164,6 @@ pub const Input = union (enum) { }; }; -pub const OpenError = std.mem.Allocator.Error || error { - -}; - -pub const Point2D = @Vector(2, f32); - -pub const Queue = struct { - commands: *device.RenderList, - - pub const State = struct { - command_index: usize, - }; - - pub fn bind(_: flow.system.BindContext) std.mem.Allocator.Error!State { - // TODO: Review how good of an idea this global state is, even if bind is guaranteed to always be ran on main. - if (renders.is_empty()) { - renders = .{.allocator = coral.heap.allocator}; - } - - const command_index = renders.len(); - - try renders.push_grow(device.RenderChain.init(coral.heap.allocator)); - - return .{ - .command_index = command_index, - }; - } - - pub fn init(state: *State) Queue { - return .{ - .commands = renders.values[state.command_index].pending(), - }; - } - - pub fn unbind(state: *State) void { - std.debug.assert(!renders.is_empty()); - - const render = &renders.values[state.command_index]; - - render.deinit(); - std.mem.swap(device.RenderChain, render, renders.get_ptr().?); - std.debug.assert(renders.pop()); - - if (renders.is_empty()) { - Queue.renders.deinit(); - } - } - - var renders = coral.stack.Sequential(device.RenderChain){.allocator = coral.heap.allocator}; -}; - pub const Rect = struct { left: f32, top: f32, @@ -218,32 +171,7 @@ pub const Rect = struct { bottom: f32, }; -pub const Transform2D = extern struct { - xbasis: Point2D = .{1, 0}, - ybasis: Point2D = .{0, 1}, - origin: Point2D = @splat(0), -}; - -const builtin_formats = [_]AssetFormat{ - .{ - .extension = "bmp", - .file_desc = formats.bmp_file_desc, - }, -}; - -pub const colors = struct { - pub const black = greyscale(0); - - pub const white = greyscale(1); - - pub fn greyscale(v: f32) Color { - return .{v, v, v, 1}; - } - - pub fn rgb(r: f32, g: f32, b: f32) Color { - return .{r, g, b, 1}; - } -}; +pub const Texture = handles.Texture; pub fn poll(app: flow.Write(App), inputs: msg.Send(Input)) !void { var event = @as(ext.SDL_Event, undefined); @@ -263,40 +191,71 @@ pub fn setup(world: *flow.World, events: App.Events) (error {Unsupported} || std return error.Unsupported; } - var context = try device.Context.init(); + const assets = create: { + var assets = try Assets.init(); - errdefer context.deinit(); + errdefer { + assets.deinit(); + } - var registered_formats = coral.stack.Sequential(AssetFormat){.allocator = coral.heap.allocator}; + break: create try world.set_get_resource(assets); + }; - errdefer registered_formats.deinit(); + assets.frame_rendered.set(); - try registered_formats.grow(builtin_formats.len); - std.debug.assert(registered_formats.push_all(&builtin_formats)); + errdefer { + assets.deinit(); + } - try world.set_resource(Assets{ - .staging_arena = std.heap.ArenaAllocator.init(coral.heap.allocator), - .formats = registered_formats, - .context = context, - }); + const builtin_texture_formats = [_]Assets.TextureFormat{ + .{ + .extension = "bmp", + .load_file = bmp.load_file, + }, + }; - try world.set_resource(Display{}); + for (builtin_texture_formats) |format| { + try assets.texture_formats.push_grow(format); + } + + try world.set_resource(Config{}); try world.on_event(events.pre_update, flow.system_fn(poll), .{.label = "poll gfx"}); try world.on_event(events.exit, flow.system_fn(stop), .{.label = "stop gfx"}); try world.on_event(events.finish, flow.system_fn(synchronize), .{.label = "synchronize gfx"}); } pub fn stop(assets: flow.Write(Assets)) void { - assets.res.staging_arena.deinit(); - assets.res.formats.deinit(); - assets.res.context.deinit(); + assets.res.deinit(); } -pub fn synchronize(assets: flow.Write(Assets), display: flow.Read(Display)) !void { - assets.res.context.submit(.{ - .width = display.res.width, - .height = display.res.height, - .clear_color = display.res.clear_color, - .renders = Queue.renders.values, +pub fn synchronize(assets: flow.Write(Assets), config: flow.Read(Config)) !void { + assets.res.frame_rendered.wait(); + assets.res.frame_rendered.reset(); + + var commands_swapped = std.Thread.ResetEvent{}; + + rendering.enqueue_work(.{ + .rotate_commands = .{ + .finished = &commands_swapped, + }, + }); + + var display_width, var display_height = [_]c_int{0, 0}; + + ext.SDL_GL_GetDrawableSize(assets.res.window, &display_width, &display_height); + + if (config.res.width != display_width or config.res.height != display_height) { + ext.SDL_SetWindowSize(assets.res.window, config.res.width, config.res.height); + } + + commands_swapped.wait(); + + rendering.enqueue_work(.{ + .render_frame = .{ + .width = config.res.width, + .height = config.res.height, + .clear_color = config.res.clear_color, + .finished = &assets.res.frame_rendered, + }, }); } diff --git a/src/ona/gfx/formats.zig b/src/ona/gfx/bmp.zig similarity index 83% rename from src/ona/gfx/formats.zig rename to src/ona/gfx/bmp.zig index 98d5952..b761a2d 100644 --- a/src/ona/gfx/formats.zig +++ b/src/ona/gfx/bmp.zig @@ -1,14 +1,10 @@ const coral = @import("coral"); -const gfx = @import("../gfx.zig"); +const handles = @import("./handles.zig"); const std = @import("std"); -pub fn bmp_file_desc( - arena: *std.heap.ArenaAllocator, - storage: coral.files.Storage, - path: []const u8, -) gfx.AssetFormat.Error!gfx.Desc { +pub fn load_file(arena: *std.heap.ArenaAllocator, storage: coral.files.Storage, path: []const u8) !handles.Texture.Desc { const header = try storage.read_little(path, 0, extern struct { type: [2]u8 align (1), file_size: u32 align (1), @@ -51,7 +47,7 @@ pub fn bmp_file_desc( while (buffer_offset < pixels.len) { const line = pixels[buffer_offset .. buffer_offset + byte_stride]; - if (try storage.read_bytes(path, file_offset, line) != byte_stride) { + if (try storage.read(path, line, file_offset) != byte_stride) { return error.FormatUnsupported; } @@ -71,15 +67,13 @@ pub fn bmp_file_desc( } return .{ - .texture = .{ - .format = .rgba8, + .format = .rgba8, - .access = .{ - .static = .{ - .width = pixel_width, - .data = pixels, - }, + .access = .{ + .static = .{ + .width = pixel_width, + .data = pixels, }, - } + }, }; } diff --git a/src/ona/gfx/colors.zig b/src/ona/gfx/colors.zig new file mode 100644 index 0000000..1682d07 --- /dev/null +++ b/src/ona/gfx/colors.zig @@ -0,0 +1,13 @@ +const lina = @import("./lina.zig"); + +pub const black = greyscale(0); + +pub const white = greyscale(1); + +pub fn greyscale(v: f32) lina.Color { + return .{v, v, v, 1}; +} + +pub fn rgb(r: f32, g: f32, b: f32) lina.Color { + return .{r, g, b, 1}; +} diff --git a/src/ona/gfx/commands.zig b/src/ona/gfx/commands.zig deleted file mode 100644 index cece28c..0000000 --- a/src/ona/gfx/commands.zig +++ /dev/null @@ -1,80 +0,0 @@ -const coral = @import("coral"); - -const std = @import("std"); - -pub fn Chain(comptime Command: type, comptime clone_command: ?Clone(Command)) type { - return struct { - swap_lists: [2]CommandList, - swap_state: u1 = 0, - - const CommandList = List(Command, clone_command); - - const Self = @This(); - - pub fn deinit(self: *Self) void { - for (&self.swap_lists) |*list| { - list.deinit(); - } - - self.* = undefined; - } - - pub fn init(allocator: std.mem.Allocator) Self { - return .{ - .swap_lists = .{CommandList.init(allocator), CommandList.init(allocator)}, - }; - } - - pub fn pending(self: *Self) *CommandList { - return &self.swap_lists[self.swap_state]; - } - - pub fn submitted(self: *Self) *CommandList { - return &self.swap_lists[self.swap_state ^ 1]; - } - - pub fn swap(self: *Self) void { - self.swap_state ^= 1; - } - }; -} - -pub fn Clone(comptime Command: type) type { - return fn (Command, *std.heap.ArenaAllocator) std.mem.Allocator.Error!Command; -} - -pub fn List(comptime Command: type, comptime clone_command: ?Clone(Command)) type { - return struct { - arena: std.heap.ArenaAllocator, - stack: coral.stack.Sequential(Command), - - const Self = @This(); - - pub fn append(self: *Self, command: Command) std.mem.Allocator.Error!void { - return self.stack.push_grow(if (clone_command) |clone| try clone(command, &self.arena) else command); - } - - pub fn clear(self: *Self) void { - self.stack.clear(); - - if (!self.arena.reset(.retain_capacity)) { - std.log.warn("failed to reset the buffer of a gfx queue with retained capacity", .{}); - } - } - - pub fn deinit(self: *Self) void { - self.arena.deinit(); - self.stack.deinit(); - - self.* = undefined; - } - - pub fn init(allocator: std.mem.Allocator) Self { - return .{ - .arena = std.heap.ArenaAllocator.init(allocator), - .stack = .{.allocator = allocator}, - }; - } - }; -} - diff --git a/src/ona/gfx/device.zig b/src/ona/gfx/device.zig deleted file mode 100644 index b5b9eca..0000000 --- a/src/ona/gfx/device.zig +++ /dev/null @@ -1,847 +0,0 @@ -const commands = @import("./commands.zig"); - -const coral = @import("coral"); - -const ext = @import("../ext.zig"); - -const gfx = @import("../gfx.zig"); - -const sokol = @import("sokol"); - -const spirv = @import("./spirv.zig"); - -const std = @import("std"); - -pub const Context = struct { - window: *ext.SDL_Window, - thread: std.Thread, - loop: *Loop, - - pub const Submission = struct { - width: u16, - height: u16, - clear_color: gfx.Color = gfx.colors.black, - renders: []RenderChain, - }; - - pub fn close(self: *Context, handle: *gfx.Handle) void { - const close_commands = self.loop.closes.pending(); - - std.debug.assert(close_commands.stack.cap > close_commands.stack.len()); - close_commands.append(handle) catch unreachable; - } - - pub fn create(self: *Context, desc: gfx.Desc) gfx.OpenError!*gfx.Handle { - const resource = try coral.heap.allocator.create(Resource); - - errdefer coral.heap.allocator.destroy(resource); - - try self.loop.creations.pending().append(.{ - .resource = resource, - .desc = desc, - }); - - const pending_destroys = self.loop.destroys.pending(); - - if (pending_destroys.stack.len() == pending_destroys.stack.cap) { - try pending_destroys.stack.grow(1); - } - - return @ptrCast(resource); - } - - pub fn deinit(self: *Context) void { - self.loop.is_running.store(false, .monotonic); - self.loop.ready.post(); - self.thread.join(); - self.loop.deinit(); - coral.heap.allocator.destroy(self.loop); - ext.SDL_DestroyWindow(self.window); - - self.* = undefined; - } - - pub fn init() !Context { - const window = create: { - const position = ext.SDL_WINDOWPOS_CENTERED; - const flags = ext.SDL_WINDOW_OPENGL; - const width = 640; - const height = 480; - - break: create ext.SDL_CreateWindow("Ona", position, position, width, height, flags) orelse { - return error.Unsupported; - }; - }; - - errdefer ext.SDL_DestroyWindow(window); - - const loop = try coral.heap.allocator.create(Loop); - - errdefer coral.heap.allocator.destroy(loop); - - loop.* = .{}; - - return .{ - .loop = loop, - .window = window, - - .thread = spawn: { - const thread = try std.Thread.spawn(.{}, Loop.run, .{loop, window}); - - thread.setName("Ona Graphics") catch { - std.log.warn("failed to name the graphics thread", .{}); - }; - - break: spawn thread; - }, - }; - } - - pub fn submit(self: *Context, submission: Submission) void { - self.loop.finished.wait(); - - defer self.loop.ready.post(); - - for (submission.renders) |*render| { - render.swap(); - } - - self.loop.creations.swap(); - self.loop.destroys.swap(); - - var last_width, var last_height = [_]c_int{0, 0}; - - ext.SDL_GetWindowSize(self.window, &last_width, &last_height); - - if (submission.width != last_width or submission.height != last_height) { - ext.SDL_SetWindowSize(self.window, submission.width, submission.height); - } - - self.loop.clear_color = submission.clear_color; - self.loop.renders = submission.renders; - - self.loop.ready.post(); - } -}; - -const Frame = struct { - projection: Projection = .{@splat(0), @splat(0), @splat(0), @splat(0)}, - flushed_batch_count: usize = 0, - pushed_batch_count: usize = 0, - render_passes: usize = 0, - mesh: ?*gfx.Handle = null, - texture: ?*gfx.Handle = null, - material: ?*gfx.Handle = null, -}; - -const Loop = struct { - ready: std.Thread.Semaphore = .{}, - finished: std.Thread.Semaphore = .{}, - clear_color: gfx.Color = gfx.colors.black, - is_running: AtomicBool = AtomicBool.init(true), - renders: []RenderChain = &.{}, - destroys: DestroyChain = DestroyChain.init(coral.heap.allocator), - creations: CreateChain = CreateChain.init(coral.heap.allocator), - has_resource_head: ?*Resource = null, - has_resource_tail: ?*Resource = null, - - const AtomicBool = std.atomic.Value(bool); - - const CreateCommand = struct { - resource: *Resource, - desc: gfx.Desc, - - fn clone(command: CreateCommand, arena: *std.heap.ArenaAllocator) std.mem.Allocator.Error!CreateCommand { - const allocator = arena.allocator(); - - return .{ - .desc = switch (command.desc) { - .texture => |texture| .{ - .texture = .{ - .access = switch (texture.access) { - .static => |static| .{ - .static = .{ - .data = try allocator.dupe(coral.io.Byte, static.data), - .width = static.width, - }, - }, - - .render => |render| .{ - .render = .{ - .width = render.width, - .height = render.height, - }, - }, - }, - - .format = texture.format, - }, - }, - - .mesh_2d => |mesh_2d| .{ - .mesh_2d = .{ - .vertices = try allocator.dupe(gfx.Desc.Mesh2D.Vertex, mesh_2d.vertices), - .indices = try allocator.dupe(u16, mesh_2d.indices), - }, - }, - }, - - .resource = command.resource, - }; - } - }; - - const CreateChain = commands.Chain(CreateCommand, CreateCommand.clone); - - const DestroyChain = commands.Chain(*Resource, null); - - fn consume_creations(self: *Loop) std.mem.Allocator.Error!void { - const create_commands = self.creations.submitted(); - - defer create_commands.clear(); - - for (create_commands.stack.values) |command| { - errdefer coral.heap.allocator.destroy(command.resource); - - command.resource.* = .{ - .payload = switch (command.desc) { - .texture => |texture| init: { - const pixel_format = switch (texture.format) { - .rgba8 => sokol.gfx.PixelFormat.RGBA8, - .bgra8 => sokol.gfx.PixelFormat.BGRA8, - }; - - switch (texture.access) { - .static => |static| { - break: init .{ - .texture = .{ - .image = sokol.gfx.makeImage(.{ - .width = static.width, - .height = @intCast(static.data.len / (static.width * texture.format.byte_size())), - .pixel_format = pixel_format, - - .data = .{ - .subimage = get: { - var subimage = [_][16]sokol.gfx.Range{.{.{}} ** 16} ** 6; - - subimage[0][0] = sokol.gfx.asRange(static.data); - - break: get subimage; - }, - }, - }), - - .sampler = sokol.gfx.makeSampler(.{}), - }, - }; - }, - - .render => |render| { - const color_image = sokol.gfx.makeImage(.{ - .width = render.width, - .height = render.height, - .render_target = true, - .pixel_format = pixel_format, - }); - - const depth_image = sokol.gfx.makeImage(.{ - .width = render.width, - .height = render.height, - .render_target = true, - .pixel_format = .DEPTH_STENCIL, - }); - - break: init .{ - .render_target = .{ - .attachments = sokol.gfx.makeAttachments(.{ - .colors = get: { - var attachments = [_]sokol.gfx.AttachmentDesc{.{}} ** 4; - - attachments[0] = .{ - .image = color_image, - }; - - break: get attachments; - }, - - .depth_stencil = .{ - .image = depth_image, - }, - }), - - .sampler = sokol.gfx.makeSampler(.{}), - .color_image = color_image, - .depth_image = depth_image, - .width = render.width, - .height = render.height, - }, - }; - }, - } - }, - - .mesh_2d => |mesh_2d| init: { - if (mesh_2d.indices.len > std.math.maxInt(u32)) { - return error.OutOfMemory; - } - - break: init .{ - .mesh_2d = .{ - .index_buffer = sokol.gfx.makeBuffer(.{ - .data = sokol.gfx.asRange(mesh_2d.indices), - .type = .INDEXBUFFER, - }), - - .vertex_buffer = sokol.gfx.makeBuffer(.{ - .data = sokol.gfx.asRange(mesh_2d.vertices), - .type = .VERTEXBUFFER, - }), - - .index_count = @intCast(mesh_2d.indices.len), - }, - }; - }, - }, - - .has_prev = self.has_resource_tail, - .has_next = null, - }; - - if (self.has_resource_tail) |resource_tail| { - resource_tail.has_next = command.resource; - } else { - std.debug.assert(self.has_resource_head == null); - - self.has_resource_head = command.resource; - } - - self.has_resource_tail = command.resource; - } - } - - fn consume_destroys(self: *Loop) std.mem.Allocator.Error!void { - const destroy_commands = self.destroys.submitted(); - - defer destroy_commands.clear(); - - for (destroy_commands.stack.values) |resource| { - defer coral.heap.allocator.destroy(resource); - - resource.destroy(); - - if (resource.has_prev) |resource_prev| { - resource_prev.has_next = resource.has_next; - } - - if (resource.has_next) |resource_next| { - resource_next.has_prev = resource.has_prev; - } - } - } - - fn deinit(self: *Loop) void { - self.destroys.deinit(); - self.creations.deinit(); - } - - fn run(self: *Loop, window: *ext.SDL_Window) !void { - const context = configure_and_create: { - var result = @as(c_int, 0); - - result |= ext.SDL_GL_SetAttribute(ext.SDL_GL_CONTEXT_FLAGS, ext.SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG); - result |= ext.SDL_GL_SetAttribute(ext.SDL_GL_CONTEXT_PROFILE_MASK, ext.SDL_GL_CONTEXT_PROFILE_CORE); - result |= ext.SDL_GL_SetAttribute(ext.SDL_GL_CONTEXT_MAJOR_VERSION, 3); - result |= ext.SDL_GL_SetAttribute(ext.SDL_GL_CONTEXT_MINOR_VERSION, 3); - result |= ext.SDL_GL_SetAttribute(ext.SDL_GL_DOUBLEBUFFER, 1); - - if (result != 0) { - return error.Unsupported; - } - - break: configure_and_create ext.SDL_GL_CreateContext(window); - }; - - sokol.gfx.setup(.{ - .environment = .{ - .defaults = .{ - .color_format = .RGBA8, - .depth_format = .DEPTH_STENCIL, - .sample_count = 1, - }, - }, - - .logger = .{ - .func = sokol.log.func, - }, - }); - - defer { - sokol.gfx.shutdown(); - ext.SDL_GL_DeleteContext(context); - } - - var rendering_2d = try Rendering2D.init(); - - defer rendering_2d.deinit(); - - self.finished.post(); - - while (self.is_running.load(.monotonic)) { - self.ready.wait(); - - defer self.finished.post(); - - try self.consume_creations(); - - var frame = Frame{}; - - rendering_2d.set_target(&frame, window, .{ - .clear_color = self.clear_color, - .clear_depth = 0, - }); - - if (self.renders.len != 0) { - const renders_tail_index = self.renders.len - 1; - - for (self.renders[0 .. renders_tail_index]) |*render| { - const render_commands = render.submitted(); - - defer render_commands.clear(); - - for (render_commands.stack.values) |command| { - try switch (command) { - .instance_2d => |instance_2d| rendering_2d.batch(&frame, instance_2d), - .target => |target| rendering_2d.set_target(&frame, window, target), - }; - } - - rendering_2d.set_target(&frame, window, .{ - .clear_color = null, - .clear_depth = null, - }); - } - - const render_commands = self.renders[renders_tail_index].submitted(); - - defer render_commands.clear(); - - for (render_commands.stack.values) |command| { - try switch (command) { - .instance_2d => |instance_2d| rendering_2d.batch(&frame, instance_2d), - .target => |target| rendering_2d.set_target(&frame, window, target), - }; - } - - rendering_2d.flush(&frame); - } - - sokol.gfx.endPass(); - sokol.gfx.commit(); - ext.SDL_GL_SwapWindow(window); - - try self.consume_destroys(); - } - - var has_next_resource = self.has_resource_head; - - while (has_next_resource) |resource| { - std.log.info("destroying remaining gfx device resource {x}", .{@intFromPtr(resource)}); - resource.destroy(); - - has_next_resource = resource.has_next; - - coral.heap.allocator.destroy(resource); - } - } -}; - -const Projection = [4]@Vector(4, f32); - -pub const RenderChain = commands.Chain(gfx.Command, clone_command); - -pub const RenderList = commands.List(gfx.Command, clone_command); - -const Rendering2D = struct { - batching_pipeline: sokol.gfx.Pipeline, - batching_buffers: coral.stack.Sequential(sokol.gfx.Buffer), - - const Instance = extern struct { - transform: gfx.Transform2D, - tint: @Vector(4, u8) = @splat(std.math.maxInt(u8)), - depth: f32 = 0, - texture_offset: gfx.Point2D = @splat(0), - texture_size: gfx.Point2D = @splat(1), - }; - - const buffer_indices = .{ - .mesh = 0, - .instance = 1, - }; - - fn batch(self: *Rendering2D, frame: *Frame, instance: gfx.Command.Instance2D) std.mem.Allocator.Error!void { - if (instance.mesh_2d != frame.mesh or instance.texture != frame.texture) { - self.flush(frame); - } - - frame.mesh = instance.mesh_2d; - frame.texture = instance.texture; - - const has_filled_buffer = (frame.pushed_batch_count % instances_per_buffer) == 0; - const pushed_buffer_count = frame.pushed_batch_count / instances_per_buffer; - - if (has_filled_buffer and pushed_buffer_count == self.batching_buffers.len()) { - var name_buffer = [_:0]u8{0} ** 64; - - const name_view = std.fmt.bufPrint(&name_buffer, "instance 2d buffer #{}", .{self.batching_buffers.len()}) catch { - unreachable; - }; - - const instance_buffer = sokol.gfx.makeBuffer(.{ - .label = name_view.ptr, - .size = @sizeOf(Instance) * instances_per_buffer, - .usage = .STREAM, - }); - - errdefer sokol.gfx.destroyBuffer(instance_buffer); - - try self.batching_buffers.push_grow(instance_buffer); - } - - _ = sokol.gfx.appendBuffer(self.batching_buffers.get().?, sokol.gfx.asRange(&Instance{ - .transform = instance.transform, - })); - - frame.pushed_batch_count += 1; - } - - fn deinit(self: *Rendering2D) void { - for (self.batching_buffers.values) |buffer| { - sokol.gfx.destroyBuffer(buffer); - } - - self.batching_buffers.deinit(); - sokol.gfx.destroyPipeline(self.batching_pipeline); - } - - fn flush(self: *Rendering2D, frame: *Frame) void { - const unflushed_count = frame.pushed_batch_count - frame.flushed_batch_count; - - if (unflushed_count == 0) { - return; - } - - sokol.gfx.applyPipeline(self.batching_pipeline); - sokol.gfx.applyUniforms(.VS, 0, sokol.gfx.asRange(&frame.projection)); - - const mesh_2d = resource_cast(frame.mesh.?).payload.mesh_2d; - - const texture_image, const texture_sampler = switch (resource_cast(frame.texture.?).payload) { - .texture => |texture| .{texture.image, texture.sampler}, - .render_target => |render_target| .{render_target.color_image, render_target.sampler}, - else => unreachable, - }; - - var bindings = sokol.gfx.Bindings{ - .vertex_buffers = get: { - var buffers = [_]sokol.gfx.Buffer{.{}} ** 8; - - buffers[buffer_indices.mesh] = mesh_2d.vertex_buffer; - - break: get buffers; - }, - - .index_buffer = mesh_2d.index_buffer, - - .fs = .{ - .images = get: { - var images = [_]sokol.gfx.Image{.{}} ** 12; - - images[0] = texture_image; - - break: get images; - }, - - .samplers = get: { - var samplers = [_]sokol.gfx.Sampler{.{}} ** 8; - - samplers[0] = texture_sampler; - - break: get samplers; - }, - }, - - .vs = .{ - .images = get: { - var images = [_]sokol.gfx.Image{.{}} ** 12; - - images[0] = texture_image; - - break: get images; - }, - - .samplers = get: { - var samplers = [_]sokol.gfx.Sampler{.{}} ** 8; - - samplers[0] = texture_sampler; - - break: get samplers; - }, - }, - }; - - while (frame.flushed_batch_count < frame.pushed_batch_count) { - const buffer_index = frame.flushed_batch_count / instances_per_buffer; - const buffer_offset = frame.flushed_batch_count % instances_per_buffer; - const instances_to_flush = @min(instances_per_buffer - buffer_offset, unflushed_count); - - bindings.vertex_buffers[buffer_indices.instance] = self.batching_buffers.values[buffer_index]; - bindings.vertex_buffer_offsets[buffer_indices.instance] = @intCast(@sizeOf(Instance) * buffer_offset); - - sokol.gfx.applyBindings(bindings); - sokol.gfx.draw(0, mesh_2d.index_count, @intCast(instances_to_flush)); - - frame.flushed_batch_count += instances_to_flush; - } - } - - fn init() spirv.Error!Rendering2D { - sokol.gfx.setup(.{ - .environment = .{ - .defaults = .{ - .color_format = .RGBA8, - .depth_format = .DEPTH_STENCIL, - .sample_count = 1, - }, - }, - - .logger = .{ - .func = sokol.log.func, - }, - }); - - var spirv_unit = try spirv.Unit.init(); - - defer spirv_unit.deinit(); - - const shader_spirv = spirv.embed_shader("./shaders/render_2d.spv"); - - try spirv_unit.compile(shader_spirv, .vertex); - - std.log.info("{s}\n", .{spirv_unit.shader_desc.vs.source}); - - try spirv_unit.compile(shader_spirv, .fragment); - - std.log.info("{s}\n", .{spirv_unit.shader_desc.fs.source}); - - return .{ - .batching_pipeline = sokol.gfx.makePipeline(.{ - .label = "2D drawing pipeline", - - .layout = .{ - .attrs = get: { - var attrs = [_]sokol.gfx.VertexAttrState{.{}} ** 16; - - attrs[0] = .{ - .format = .FLOAT2, - .buffer_index = buffer_indices.mesh, - }; - - attrs[1] = .{ - .format = .FLOAT2, - .buffer_index = buffer_indices.mesh, - }; - - attrs[2] = .{ - .format = .FLOAT2, - .buffer_index = buffer_indices.instance, - }; - - attrs[3] = .{ - .format = .FLOAT2, - .buffer_index = buffer_indices.instance, - }; - - attrs[4] = .{ - .format = .FLOAT2, - .buffer_index = buffer_indices.instance, - }; - - attrs[5] = .{ - .format = .UBYTE4N, - .buffer_index = buffer_indices.instance, - }; - - attrs[6] = .{ - .format = .FLOAT, - .buffer_index = buffer_indices.instance, - }; - - attrs[7] = .{ - .format = .FLOAT4, - .buffer_index = buffer_indices.instance, - }; - - break: get attrs; - }, - - .buffers = get: { - var buffers = [_]sokol.gfx.VertexBufferLayoutState{.{}} ** 8; - - buffers[buffer_indices.instance].step_func = .PER_INSTANCE; - - break: get buffers; - }, - }, - - .shader = sokol.gfx.makeShader(spirv_unit.shader_desc), - .index_type = .UINT16, - }), - - .batching_buffers = .{.allocator = coral.heap.allocator}, - }; - } - - const instances_per_buffer = 512; - - fn set_target(self: *Rendering2D, frame: *Frame, window: *ext.SDL_Window, target: gfx.Command.Target) void { - defer frame.render_passes += 1; - - if (frame.render_passes != 0) { - self.flush(frame); - sokol.gfx.endPass(); - } - - var pass = sokol.gfx.Pass{ - .action = .{ - .stencil = .{.load_action = .CLEAR}, - }, - }; - - if (target.clear_color) |color| { - pass.action.colors[0] = .{ - .load_action = .CLEAR, - .clear_value = @bitCast(color), - }; - } else { - pass.action.colors[0] = .{.load_action = .LOAD}; - } - - if (target.clear_depth) |depth| { - pass.action.depth = .{ - .load_action = .CLEAR, - .clear_value = depth, - }; - } else { - pass.action.depth = .{.load_action = .LOAD}; - } - - if (target.texture) |render_texture| { - const render_target = resource_cast(render_texture).payload.render_target; - - pass.attachments = render_target.attachments; - - frame.projection = orthographic_projection(-1.0, 1.0, .{ - .left = 0, - .top = 0, - .right = @floatFromInt(render_target.width), - .bottom = @floatFromInt(render_target.height), - }); - } else { - var target_width, var target_height = [_]c_int{0, 0}; - - ext.SDL_GL_GetDrawableSize(window, &target_width, &target_height); - std.debug.assert(target_width > 0 and target_height > 0); - - pass.swapchain = .{ - .width = target_width, - .height = target_height, - .sample_count = 1, - .color_format = .RGBA8, - .depth_format = .DEPTH_STENCIL, - .gl = .{.framebuffer = 0}, - }; - - frame.projection = orthographic_projection(-1.0, 1.0, .{ - .left = 0, - .top = 0, - .right = @floatFromInt(target_width), - .bottom = @floatFromInt(target_height), - }); - } - - sokol.gfx.beginPass(pass); - } -}; - -const Resource = struct { - has_prev: ?*Resource, - has_next: ?*Resource, - - payload: union (enum) { - mesh_2d: struct { - index_count: u32, - vertex_buffer: sokol.gfx.Buffer, - index_buffer: sokol.gfx.Buffer, - }, - - texture: struct { - image: sokol.gfx.Image, - sampler: sokol.gfx.Sampler, - }, - - render_target: struct { - sampler: sokol.gfx.Sampler, - color_image: sokol.gfx.Image, - depth_image: sokol.gfx.Image, - attachments: sokol.gfx.Attachments, - width: u16, - height: u16, - }, - }, - - fn destroy(self: Resource) void { - switch (self.payload) { - .mesh_2d => |mesh_2d| { - sokol.gfx.destroyBuffer(mesh_2d.vertex_buffer); - sokol.gfx.destroyBuffer(mesh_2d.index_buffer); - }, - - .texture => |texture| { - sokol.gfx.destroyImage(texture.image); - sokol.gfx.destroySampler(texture.sampler); - }, - - .render_target => |render_target| { - sokol.gfx.destroyImage(render_target.color_image); - sokol.gfx.destroyImage(render_target.depth_image); - sokol.gfx.destroySampler(render_target.sampler); - sokol.gfx.destroyAttachments(render_target.attachments); - }, - } - } -}; - -fn clone_command(self: gfx.Command, arena: *std.heap.ArenaAllocator) std.mem.Allocator.Error!gfx.Command { - _ = arena; - - return switch (self) { - .instance_2d => |instance_2d| .{.instance_2d = instance_2d}, - .target => |target| .{.target = target}, - }; -} - -fn orthographic_projection(near: f32, far: f32, viewport: gfx.Rect) Projection { - const width = viewport.right - viewport.left; - const height = viewport.bottom - viewport.top; - - return .{ - .{2 / width, 0, 0, 0}, - .{0, 2 / height, 0, 0}, - .{0, 0, 1 / (far - near), 0}, - .{-((viewport.left + viewport.right) / width), -((viewport.top + viewport.bottom) / height), near / (near - far), 1}, - }; -} - -fn resource_cast(handle: *gfx.Handle) *Resource { - return @ptrCast(@alignCast(handle)); -} diff --git a/src/ona/gfx/handles.zig b/src/ona/gfx/handles.zig new file mode 100644 index 0000000..8c8a014 --- /dev/null +++ b/src/ona/gfx/handles.zig @@ -0,0 +1,45 @@ +const coral = @import("coral"); + +const std = @import("std"); + +fn Handle(comptime HandleDesc: type) type { + return enum (u32) { + default, + _, + + pub const Desc = HandleDesc; + + const Self = @This(); + }; +} + +pub const Texture = Handle(struct { + format: Format, + access: Access, + + pub const Access = union (enum) { + static: Static, + render: Render, + + pub const Static = struct { + width: u16, + data: []const coral.io.Byte, + }; + + pub const Render = struct { + width: u16, + height: u16, + }; + }; + + pub const Format = enum { + rgba8, + bgra8, + + pub fn byte_size(self: Format) usize { + return switch (self) { + .rgba8, .bgra8 => 4, + }; + } + }; +}); diff --git a/src/ona/gfx/lina.zig b/src/ona/gfx/lina.zig new file mode 100644 index 0000000..3020f76 --- /dev/null +++ b/src/ona/gfx/lina.zig @@ -0,0 +1,29 @@ +const gfx = @import("../gfx.zig"); + +pub const Color = @Vector(4, f32); + +pub fn EvenOrderMatrix(comptime n: usize, comptime Element: type) type { + return [n]@Vector(n, Element); +} + +pub const Transform2D = extern struct { + xbasis: Vector = .{1, 0}, + ybasis: Vector = .{0, 1}, + origin: Vector = @splat(0), + + const Vector = @Vector(2, f32); +}; + +pub const ProjectionMatrix = EvenOrderMatrix(4, f32); + +pub fn orthographic_projection(near: f32, far: f32, viewport: gfx.Rect) EvenOrderMatrix(4, f32) { + const width = viewport.right - viewport.left; + const height = viewport.bottom - viewport.top; + + return .{ + .{2 / width, 0, 0, 0}, + .{0, 2 / height, 0, 0}, + .{0, 0, 1 / (far - near), 0}, + .{-((viewport.left + viewport.right) / width), -((viewport.top + viewport.bottom) / height), near / (near - far), 1}, + }; +} diff --git a/src/ona/gfx/rendering.zig b/src/ona/gfx/rendering.zig new file mode 100644 index 0000000..6727443 --- /dev/null +++ b/src/ona/gfx/rendering.zig @@ -0,0 +1,662 @@ +const coral = @import("coral"); + +const ext = @import("../ext.zig"); + +const handles = @import("./handles.zig"); + +const lina = @import("./lina.zig"); + +const resources = @import("./resources.zig"); + +const spirv = @import("./spirv.zig"); + +const sokol = @import("sokol"); + +const std = @import("std"); + +pub const Command = union (enum) { + draw_texture: DrawTexture, + set_target: SetTarget, + + pub const DrawTexture = struct { + texture: handles.Texture, + transform: lina.Transform2D, + }; + + pub const SetTarget = struct { + texture: ?handles.Texture = null, + clear_color: ?lina.Color, + clear_depth: ?f32, + clear_stencil: ?u8, + }; + + fn clone(self: Command, arena: *std.heap.ArenaAllocator) !Command { + _ = arena; + + return switch (self) { + .draw_texture => |draw_texture| .{.draw_texture = draw_texture}, + .set_target => |set_target| .{.set_target = set_target}, + }; + } + + fn process(self: Command, pools: *Pools, frame: *Frame) !void { + try switch (self) { + .draw_texture => |draw_texture| frame.draw_texture(pools, draw_texture), + .set_target => |set_target| frame.set_target(pools, set_target), + }; + } +}; + +pub const Commands = struct { + swap_lists: [2]List, + swap_state: u1 = 0, + has_next: ?*Commands = null, + has_prev: ?*Commands = null, + + pub const List = struct { + arena: std.heap.ArenaAllocator, + stack: coral.stack.Sequential(Command), + + pub fn append(self: *List, command: Command) std.mem.Allocator.Error!void { + return self.stack.push_grow(try command.clone(&self.arena)); + } + + pub fn clear(self: *List) void { + self.stack.clear(); + + if (!self.arena.reset(.retain_capacity)) { + std.log.warn("failed to reset the buffer of a gfx queue with retained capacity", .{}); + } + } + + fn deinit(self: *List) void { + self.arena.deinit(); + self.stack.deinit(); + + self.* = undefined; + } + + fn init(allocator: std.mem.Allocator) List { + return .{ + .arena = std.heap.ArenaAllocator.init(allocator), + .stack = .{.allocator = allocator}, + }; + } + }; + + const Self = @This(); + + pub fn deinit(self: *Self) void { + for (&self.swap_lists) |*list| { + list.deinit(); + } + + self.* = undefined; + } + + pub fn init(allocator: std.mem.Allocator) Self { + return .{ + .swap_lists = .{List.init(allocator), List.init(allocator)}, + }; + } + + pub fn pending_list(self: *Self) *List { + return &self.swap_lists[self.swap_state]; + } + + fn rotate(self: *Self) void { + const swapped_state = self.swap_state ^ 1; + + self.swap_lists[swapped_state].clear(); + + self.swap_state = swapped_state; + } + + pub fn submitted_commands(self: *const Self) []const Command { + return self.swap_lists[self.swap_state ^ 1].stack.values; + } +}; + +const Frame = struct { + swapchain: sokol.gfx.Swapchain, + drawn_count: usize = 0, + flushed_count: usize = 0, + current_source_texture: handles.Texture = .default, + current_target_texture: ?handles.Texture = null, + + const DrawTexture = extern struct { + transform: lina.Transform2D, + tint: @Vector(4, u8) = @splat(std.math.maxInt(u8)), + depth: f32 = 0, + texture_offset: @Vector(2, f32) = @splat(0), + texture_size: @Vector(2, f32) = @splat(1), + }; + + pub fn draw_texture(self: *Frame, pools: *Pools, command: Command.DrawTexture) !void { + if (command.texture != self.current_source_texture) { + self.flush(pools); + } + + self.current_source_texture = command.texture; + + const has_filled_current_buffer = (self.drawn_count % batches_per_buffer) == 0; + const buffer_count = self.drawn_count / batches_per_buffer; + + if (has_filled_current_buffer and buffer_count == texture_batch_buffers.len()) { + const instance_buffer = sokol.gfx.makeBuffer(.{ + .size = @sizeOf(DrawTexture) * batches_per_buffer, + .usage = .STREAM, + }); + + errdefer sokol.gfx.destroyBuffer(instance_buffer); + + try texture_batch_buffers.push_grow(instance_buffer); + } + + _ = sokol.gfx.appendBuffer(texture_batch_buffers.get().?, sokol.gfx.asRange(&DrawTexture{ + .transform = command.transform, + })); + + self.drawn_count += 1; + } + + pub fn flush(self: *Frame, pools: *Pools) void { + if (self.flushed_count == self.drawn_count) { + return; + } + + var bindings = sokol.gfx.Bindings{ + .index_buffer = quad_index_buffer, + }; + + bindings.vertex_buffers[vertex_indices.mesh] = quad_vertex_buffer; + + switch (pools.textures.get(@intFromEnum(self.current_source_texture)).?.*) { + .render => |render| { + bindings.fs.images[0] = render.color_image; + bindings.fs.samplers[0] = render.sampler; + bindings.vs.images[0] = render.color_image; + bindings.vs.samplers[0] = render.sampler; + }, + + .static => |static| { + bindings.fs.images[0] = static.image; + bindings.fs.samplers[0] = static.sampler; + bindings.vs.images[0] = static.image; + bindings.vs.samplers[0] = static.sampler; + }, + } + + if (self.current_target_texture) |target_texture| { + const target_view_buffer = pools.textures.get(@intFromEnum(target_texture)).?.render.view_buffer; + + bindings.fs.storage_buffers[0] = target_view_buffer; + bindings.vs.storage_buffers[0] = target_view_buffer; + } else { + bindings.fs.storage_buffers[0] = view_buffer; + bindings.vs.storage_buffers[0] = view_buffer; + } + + sokol.gfx.applyPipeline(texture_batching_pipeline); + + while (true) { + const buffer_index = self.flushed_count / batches_per_buffer; + const buffer_offset = self.flushed_count % batches_per_buffer; + const instances_to_flush = @min(batches_per_buffer - buffer_offset, self.drawn_count - self.flushed_count); + + self.flushed_count += instances_to_flush; + bindings.vertex_buffers[vertex_indices.instance] = texture_batch_buffers.values[buffer_index]; + bindings.vertex_buffer_offsets[vertex_indices.instance] = @intCast(@sizeOf(DrawTexture) * buffer_offset); + + sokol.gfx.applyBindings(bindings); + sokol.gfx.draw(0, 6, @intCast(instances_to_flush)); + + if (self.flushed_count == self.drawn_count) { + break; + } + } + } + + pub fn set_target(self: *Frame, pools: *Pools, command: Command.SetTarget) void { + sokol.gfx.endPass(); + + var pass = sokol.gfx.Pass{ + .action = .{.stencil = .{.load_action = .CLEAR}}, + }; + + if (command.clear_color) |color| { + pass.action.colors[0] = .{ + .load_action = .CLEAR, + .clear_value = @bitCast(color), + }; + } else { + pass.action.colors[0] = .{.load_action = .LOAD}; + } + + if (command.clear_depth) |depth| { + pass.action.depth = .{ + .load_action = .CLEAR, + .clear_value = depth, + }; + } else { + pass.action.depth = .{.load_action = .LOAD}; + } + + if (command.texture) |texture| { + pass.attachments = switch (pools.textures.get(@intFromEnum(texture)).?.*) { + .static => @panic("Cannot render to static textures"), + .render => |render| render.attachments, + }; + + self.current_target_texture = command.texture; + } else { + pass.swapchain = self.swapchain; + self.current_target_texture = null; + } + + sokol.gfx.beginPass(pass); + } + + const storage_bindings = .{ + .engine = 0, + .material = 1, + }; + + var texture_batch_buffers = coral.stack.Sequential(sokol.gfx.Buffer){.allocator = coral.heap.allocator}; + + const batches_per_buffer = 512; + + var texture_batching_pipeline: sokol.gfx.Pipeline = undefined; +}; + +const Pools = struct { + textures: TexturePool, + + const TexturePool = coral.Pool(resources.Texture); + + fn create_texture(self: *Pools, desc: handles.Texture.Desc) !handles.Texture { + var texture = try resources.Texture.init(desc); + + errdefer texture.deinit(); + + return @enumFromInt(try self.textures.insert(texture)); + } + + fn deinit(self: *Pools) void { + var textures = self.textures.values(); + + while (textures.next()) |texture| { + texture.deinit(); + } + + self.textures.deinit(); + } + + fn destroy_texture(self: *Pools, texture_key: handles.Texture) bool { + switch (texture_key) { + .default => {}, + + else => { + var texture = self.textures.remove(@intFromEnum(texture_key)) orelse { + return false; + }; + + texture.deinit(); + }, + } + + return true; + } + + fn init(allocator: std.mem.Allocator) !Pools { + var pools = Pools{ + .textures = TexturePool.init(allocator), + }; + + errdefer pools.deinit(); + + const default_texture_data = [_]u32{ + 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, + 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, + 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, + 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, + 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, + 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, + 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, + 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, + }; + + _ = try pools.create_texture(.{ + .format = .rgba8, + + .access = .{ + .static = .{ + .data = std.mem.asBytes(&default_texture_data), + .width = 8, + }, + }, + }); + + return pools; + } +}; + +pub const Work = union (enum) { + create_commands: CreateCommandsWork, + load_texture: LoadTextureWork, + render_frame: RenderFrameWork, + rotate_commands: RotateCommandsWork, + shutdown, + unload_texture: UnloadTextureWork, + + pub const CreateCommandsWork = struct { + created: *coral.asyncio.Future(std.mem.Allocator.Error!*Commands), + }; + + pub const LoadTextureWork = struct { + desc: handles.Texture.Desc, + loaded: *coral.asyncio.Future(std.mem.Allocator.Error!handles.Texture), + }; + + pub const RotateCommandsWork = struct { + finished: *std.Thread.ResetEvent, + }; + + pub const RenderFrameWork = struct { + clear_color: lina.Color, + width: u16, + height: u16, + finished: *std.Thread.ResetEvent, + }; + + pub const UnloadTextureWork = struct { + handle: handles.Texture, + }; + + var pending: coral.asyncio.BlockingQueue(1024, Work) = .{}; +}; + +pub fn enqueue_work(work: Work) void { + Work.pending.enqueue(work); +} + +pub fn startup(window: *ext.SDL_Window) !void { + work_thread = try std.Thread.spawn(.{}, run, .{window}); +} + +var quad_index_buffer: sokol.gfx.Buffer = undefined; + +var quad_vertex_buffer: sokol.gfx.Buffer = undefined; + +var view_buffer: sokol.gfx.Buffer = undefined; + +const vertex_indices = .{ + .mesh = 0, + .instance = 1, +}; + +const vertex_layout_state = sokol.gfx.VertexLayoutState{ + .attrs = get: { + var attrs = [_]sokol.gfx.VertexAttrState{.{}} ** 16; + + attrs[0] = .{ + .format = .FLOAT2, + .buffer_index = vertex_indices.mesh, + }; + + attrs[1] = .{ + .format = .FLOAT2, + .buffer_index = vertex_indices.mesh, + }; + + attrs[2] = .{ + .format = .FLOAT2, + .buffer_index = vertex_indices.instance, + }; + + attrs[3] = .{ + .format = .FLOAT2, + .buffer_index = vertex_indices.instance, + }; + + attrs[4] = .{ + .format = .FLOAT2, + .buffer_index = vertex_indices.instance, + }; + + attrs[5] = .{ + .format = .UBYTE4N, + .buffer_index = vertex_indices.instance, + }; + + attrs[6] = .{ + .format = .FLOAT, + .buffer_index = vertex_indices.instance, + }; + + attrs[7] = .{ + .format = .FLOAT4, + .buffer_index = vertex_indices.instance, + }; + + break: get attrs; + }, + + .buffers = get: { + var vertex_buffer_layouts = [_]sokol.gfx.VertexBufferLayoutState{.{}} ** 8; + + vertex_buffer_layouts[vertex_indices.instance].step_func = .PER_INSTANCE; + + break: get vertex_buffer_layouts; + }, +}; + +fn run(window: *ext.SDL_Window) !void { + const context = configure_and_create: { + var result = @as(c_int, 0); + + result |= ext.SDL_GL_SetAttribute(ext.SDL_GL_CONTEXT_FLAGS, ext.SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG); + result |= ext.SDL_GL_SetAttribute(ext.SDL_GL_CONTEXT_PROFILE_MASK, ext.SDL_GL_CONTEXT_PROFILE_CORE); + result |= ext.SDL_GL_SetAttribute(ext.SDL_GL_CONTEXT_MAJOR_VERSION, 3); + result |= ext.SDL_GL_SetAttribute(ext.SDL_GL_CONTEXT_MINOR_VERSION, 3); + result |= ext.SDL_GL_SetAttribute(ext.SDL_GL_DOUBLEBUFFER, 1); + + if (result != 0) { + return error.Unsupported; + } + + break: configure_and_create ext.SDL_GL_CreateContext(window); + }; + + defer ext.SDL_GL_DeleteContext(context); + + sokol.gfx.setup(.{ + .environment = .{ + .defaults = .{ + .color_format = .RGBA8, + .depth_format = .DEPTH_STENCIL, + .sample_count = 1, + }, + }, + + .logger = .{ + .func = sokol.log.func, + }, + }); + + defer { + sokol.gfx.shutdown(); + } + + var pools = try Pools.init(coral.heap.allocator); + + defer { + pools.deinit(); + } + + const Vertex = struct { + xy: @Vector(2, f32), + uv: @Vector(2, f32), + }; + + const quad_indices = [_]u16{0, 1, 2, 0, 2, 3}; + + const quad_vertices = [_]Vertex{ + .{.xy = .{-0.5, -0.5}, .uv = .{0, 1}}, + .{.xy = .{0.5, -0.5}, .uv = .{1, 1}}, + .{.xy = .{0.5, 0.5}, .uv = .{1, 0}}, + .{.xy = .{-0.5, 0.5}, .uv = .{0, 0}}, + }; + + quad_index_buffer = sokol.gfx.makeBuffer(.{ + .data = sokol.gfx.asRange(&quad_indices), + .type = .INDEXBUFFER, + }); + + quad_vertex_buffer = sokol.gfx.makeBuffer(.{ + .data = sokol.gfx.asRange(&quad_vertices), + .type = .VERTEXBUFFER, + }); + + view_buffer = sokol.gfx.makeBuffer(.{ + .data = sokol.gfx.asRange(&lina.orthographic_projection(-1.0, 1.0, .{ + .left = 0, + .top = 0, + .right = @floatFromInt(1280), + .bottom = @floatFromInt(720), + })), + + .type = .STORAGEBUFFER, + .usage = .IMMUTABLE, + }); + + const shader_spirv = @embedFile("./shaders/2d_default.spv"); + var spirv_unit = try spirv.Unit.init(coral.heap.allocator); + + defer spirv_unit.deinit(); + + try spirv_unit.compile(shader_spirv, .vertex); + try spirv_unit.compile(shader_spirv, .fragment); + + Frame.texture_batching_pipeline = sokol.gfx.makePipeline(.{ + .label = "2D drawing pipeline", + .layout = vertex_layout_state, + .shader = sokol.gfx.makeShader(spirv_unit.shader_desc), + .index_type = .UINT16, + }); + + var has_commands_head: ?*Commands = null; + var has_commands_tail: ?*Commands = null; + + while (true) { + switch (Work.pending.dequeue()) { + .create_commands => |create_commands| { + const commands = coral.heap.allocator.create(Commands) catch { + _ = create_commands.created.resolve(error.OutOfMemory); + + continue; + }; + + commands.* = Commands.init(coral.heap.allocator); + + if (create_commands.created.resolve(commands)) { + if (has_commands_tail) |tail_commands| { + tail_commands.has_next = commands; + commands.has_prev = tail_commands; + } else { + std.debug.assert(has_commands_head == null); + + has_commands_head = commands; + } + + has_commands_tail = commands; + } else { + coral.heap.allocator.destroy(commands); + } + }, + + .load_texture => |load| { + const texture = try pools.create_texture(load.desc); + + if (!load.loaded.resolve(texture)) { + std.debug.assert(pools.destroy_texture(texture)); + } + }, + + .render_frame => |render_frame| { + var frame = Frame{ + .swapchain = .{ + .width = render_frame.width, + .height = render_frame.height, + .sample_count = 1, + .color_format = .RGBA8, + .depth_format = .DEPTH_STENCIL, + .gl = .{.framebuffer = 0}, + } + }; + + sokol.gfx.beginPass(swapchain_pass: { + var pass = sokol.gfx.Pass{ + .swapchain = frame.swapchain, + + .action = .{ + .stencil = .{.load_action = .CLEAR}, + .depth = .{.load_action = .CLEAR}, + }, + }; + + pass.action.colors[0] = .{ + .clear_value = @bitCast(render_frame.clear_color), + .load_action = .CLEAR, + }; + + break: swapchain_pass pass; + }); + + var has_commands = has_commands_head; + + while (has_commands) |commands| : (has_commands = commands.has_next) { + for (commands.submitted_commands()) |command| { + try command.process(&pools, &frame); + } + + if (frame.current_target_texture != .default) { + frame.set_target(&pools, .{ + .clear_color = null, + .clear_depth = null, + .clear_stencil = null, + }); + } + } + + frame.flush(&pools); + sokol.gfx.endPass(); + sokol.gfx.commit(); + ext.SDL_GL_SwapWindow(window); + render_frame.finished.set(); + }, + + .rotate_commands => |rotate_commands| { + var has_commands = has_commands_head; + + while (has_commands) |commands| : (has_commands = commands.has_next) { + commands.rotate(); + } + + rotate_commands.finished.set(); + }, + + .shutdown => { + break; + }, + + .unload_texture => |unload| { + if (!pools.destroy_texture(unload.handle)) { + @panic("Attempt to unload a non-existent texture"); + } + }, + } + } +} + +var work_thread: std.Thread = undefined; diff --git a/src/ona/gfx/resources.zig b/src/ona/gfx/resources.zig new file mode 100644 index 0000000..6fc9942 --- /dev/null +++ b/src/ona/gfx/resources.zig @@ -0,0 +1,149 @@ +const coral = @import("coral"); + +const handles = @import("./handles.zig"); + +const lina = @import("./lina.zig"); + +const sokol = @import("sokol"); + +const spirv = @import("./spirv.zig"); + +const std = @import("std"); + +pub const Texture = union (enum) { + render: Render, + static: Static, + + const Render = struct { + sampler: sokol.gfx.Sampler, + color_image: sokol.gfx.Image, + depth_image: sokol.gfx.Image, + attachments: sokol.gfx.Attachments, + view_buffer: sokol.gfx.Buffer, + + fn deinit(self: *Render) void { + sokol.gfx.destroyImage(self.color_image); + sokol.gfx.destroyImage(self.depth_image); + sokol.gfx.destroySampler(self.sampler); + sokol.gfx.destroyAttachments(self.attachments); + sokol.gfx.destroyBuffer(self.view_buffer); + + self.* = undefined; + } + + fn init(format: handles.Texture.Desc.Format, access: handles.Texture.Desc.Access.Render) std.mem.Allocator.Error!Render { + const color_image = sokol.gfx.makeImage(.{ + .pixel_format = switch (format) { + .rgba8 => sokol.gfx.PixelFormat.RGBA8, + .bgra8 => sokol.gfx.PixelFormat.BGRA8, + }, + + .width = access.width, + .height = access.height, + .render_target = true, + }); + + const depth_image = sokol.gfx.makeImage(.{ + .width = access.width, + .height = access.height, + .render_target = true, + .pixel_format = .DEPTH_STENCIL, + }); + + const attachments = sokol.gfx.makeAttachments(attachments_desc: { + var desc = sokol.gfx.AttachmentsDesc{ + .depth_stencil = .{ + .image = depth_image, + }, + }; + + desc.colors[0] = .{ + .image = color_image, + }; + + break: attachments_desc desc; + }); + + const sampler = sokol.gfx.makeSampler(.{}); + + const view_buffer = sokol.gfx.makeBuffer(.{ + .data = sokol.gfx.asRange(&lina.orthographic_projection(-1.0, 1.0, .{ + .left = 0, + .top = 0, + .right = @floatFromInt(access.width), + .bottom = @floatFromInt(access.height), + })), + + .type = .STORAGEBUFFER, + .usage = .IMMUTABLE, + }); + + return .{ + .attachments = attachments, + .sampler = sampler, + .color_image = color_image, + .depth_image = depth_image, + .view_buffer = view_buffer, + }; + } + }; + + const Static = struct { + image: sokol.gfx.Image, + sampler: sokol.gfx.Sampler, + + fn deinit(self: *Static) void { + sokol.gfx.destroyImage(self.image); + sokol.gfx.destroySampler(self.sampler); + + self.* = undefined; + } + + fn init(format: handles.Texture.Desc.Format, access: handles.Texture.Desc.Access.Static) std.mem.Allocator.Error!Static { + const image = sokol.gfx.makeImage(image_desc: { + var desc = sokol.gfx.ImageDesc{ + .height = std.math.cast(i32, access.data.len / (access.width * format.byte_size())) orelse { + return error.OutOfMemory; + }, + + .pixel_format = switch (format) { + .rgba8 => sokol.gfx.PixelFormat.RGBA8, + .bgra8 => sokol.gfx.PixelFormat.BGRA8, + }, + + .width = access.width, + }; + + desc.data.subimage[0][0] = sokol.gfx.asRange(access.data); + + break: image_desc desc; + }); + + const sampler = sokol.gfx.makeSampler(.{}); + + errdefer { + sokol.gfx.destroySampler(sampler); + sokol.gfx.destroyImage(image); + } + + return .{ + .image = image, + .sampler = sampler, + }; + } + }; + + pub fn deinit(self: *Texture) void { + switch (self.*) { + .static => |*static| static.deinit(), + .render => |*render| render.deinit(), + } + } + + pub fn init(desc: handles.Texture.Desc) std.mem.Allocator.Error!Texture { + return switch (desc.access) { + .static => |static| .{.static = try Static.init(desc.format, static)}, + .render => |render| .{.render = try Render.init(desc.format, render)}, + }; + } +}; diff --git a/src/ona/gfx/shaders/render_2d.frag b/src/ona/gfx/shaders/2d_default.frag similarity index 78% rename from src/ona/gfx/shaders/render_2d.frag rename to src/ona/gfx/shaders/2d_default.frag index 40e5c20..0ed5a9d 100644 --- a/src/ona/gfx/shaders/render_2d.frag +++ b/src/ona/gfx/shaders/2d_default.frag @@ -7,6 +7,10 @@ layout (location = 1) in vec2 uv; layout (location = 0) out vec4 texel; +layout (binding = 0) readonly buffer View { + mat4 projection_matrix; +}; + void main() { texel = texture(sprite, uv) * color; diff --git a/src/ona/gfx/shaders/render_2d.vert b/src/ona/gfx/shaders/2d_default.vert similarity index 95% rename from src/ona/gfx/shaders/render_2d.vert rename to src/ona/gfx/shaders/2d_default.vert index f7d730e..47cf412 100644 --- a/src/ona/gfx/shaders/render_2d.vert +++ b/src/ona/gfx/shaders/2d_default.vert @@ -13,7 +13,7 @@ layout (location = 7) in vec4 instance_rect; layout (location = 0) out vec4 color; layout (location = 1) out vec2 uv; -layout (binding = 0) uniform Projection { +layout (binding = 0) readonly buffer View { mat4 projection_matrix; }; diff --git a/src/ona/gfx/spirv.zig b/src/ona/gfx/spirv.zig index e47f631..bda5831 100644 --- a/src/ona/gfx/spirv.zig +++ b/src/ona/gfx/spirv.zig @@ -12,14 +12,22 @@ pub const Error = std.mem.Allocator.Error || error { UnsupportedTarget, InvalidSPIRV, UnsupportedSPIRV, + UnsupportedBackend, }; pub const Unit = struct { + arena: std.heap.ArenaAllocator, context: ext.spvc_context, shader_desc: sokol.gfx.ShaderDesc, attrs_used: u32 = 0, - pub fn compile(self: *Unit, spirv: []const u32, stage: Stage) Error!void { + pub fn compile(self: *Unit, spirv_data: []const u8, stage: Stage) Error!void { + if ((spirv_data.len % @alignOf(u32)) != 0) { + return error.InvalidSPIRV; + } + + const spirv_ops: []const u32 = @alignCast(std.mem.bytesAsSlice(u32, spirv_data)); + const execution_model, const stage_desc = switch (stage) { .vertex => .{ext.SpvExecutionModelVertex, &self.shader_desc.vs}, .fragment => .{ext.SpvExecutionModelFragment, &self.shader_desc.fs}, @@ -30,7 +38,7 @@ pub const Unit = struct { option_values: []const struct {ext.spvc_compiler_option, c_uint}, }; - const backend: Backend = switch (sokol.gfx.queryBackend()) { + const backend: Backend = try switch (sokol.gfx.queryBackend()) { .GLCORE => .{ .target = ext.SPVC_BACKEND_GLSL, @@ -43,13 +51,13 @@ pub const Unit = struct { }, }, - else => @panic("Unimplemented"), + else => error.UnsupportedBackend, }; const compiler = parse_and_configure: { var parsed_ir: ext.spvc_parsed_ir = null; - try to_error(ext.spvc_context_parse_spirv(self.context, spirv.ptr, spirv.len, &parsed_ir)); + try to_error(ext.spvc_context_parse_spirv(self.context, spirv_ops.ptr, spirv_ops.len, &parsed_ir)); var compiler: ext.spvc_compiler = null; @@ -60,17 +68,14 @@ pub const Unit = struct { try to_error(ext.spvc_compiler_get_combined_image_samplers(compiler, @ptrCast(&combined_image_samplers.ptr), &combined_image_samplers.len)); + const arena_allocator = self.arena.allocator(); var binding: u32 = 0; for (combined_image_samplers) |combined_image_sampler| { - var name_buffer = [_:0]u8{0} ** 255; - - const name = coral.utf8.print_formatted(&name_buffer, "{image_name}_{sampler_name}", .{ - .image_name = coral.io.slice_sentineled(u8, 0, ext.spvc_compiler_get_name(compiler, combined_image_sampler.image_id)), - .sampler_name = coral.io.slice_sentineled(u8, 0, ext.spvc_compiler_get_name(compiler, combined_image_sampler.sampler_id)), - }) catch { - return error.InvalidSPIRV; - }; + const name = try coral.utf8.alloc_formatted(arena_allocator, "{image_name}_{sampler_name}", .{ + .image_name = std.mem.span(@as([*:0]const u8, ext.spvc_compiler_get_name(compiler, combined_image_sampler.image_id))), + .sampler_name = std.mem.span(@as([*:0]const u8, ext.spvc_compiler_get_name(compiler, combined_image_sampler.sampler_id))), + }); ext.spvc_compiler_set_name(compiler, combined_image_sampler.combined_id, name); ext.spvc_compiler_set_decoration(compiler, combined_image_sampler.combined_id, ext.SpvDecorationBinding, binding); @@ -91,7 +96,8 @@ pub const Unit = struct { break: create resources; }; - try reflect_uniform_blocks(compiler, resources, stage_desc); + try reflect_uniform_buffers(compiler, resources, stage_desc); + try reflect_storage_buffers(compiler, resources, stage_desc); try reflect_image_samplers(compiler, resources, stage_desc); try to_error(ext.spvc_compiler_install_compiler_options(compiler, create: { @@ -109,15 +115,18 @@ pub const Unit = struct { })); try to_error(ext.spvc_compiler_compile(compiler, @ptrCast(&stage_desc.source))); + + std.log.info("{s}", .{stage_desc.source}); } pub fn deinit(self: *Unit) void { ext.spvc_context_destroy(self.context); + self.arena.deinit(); self.* = undefined; } - pub fn init() std.mem.Allocator.Error!Unit { + pub fn init(allocator: std.mem.Allocator) std.mem.Allocator.Error!Unit { var context: ext.spvc_context = null; if (ext.spvc_context_create(&context) != ext.SPVC_SUCCESS) { @@ -129,6 +138,7 @@ pub const Unit = struct { ext.spvc_context_set_error_callback(context, log_context_errors, null); return .{ + .arena = std.heap.ArenaAllocator.init(allocator), .context = context, .shader_desc = .{ @@ -144,44 +154,33 @@ pub const Stage = enum { vertex, }; -pub fn embed_shader(comptime path: []const u8) []const u32 { - const shader = @embedFile(path); - const alignment = @alignOf(u32); - - if ((shader.len % alignment) != 0) { - @compileError("size of file contents at " ++ path ++ " must be aligned to a multiple of 4"); - } - - return @as(*const [shader.len / alignment]u32, @ptrCast(@alignCast(shader))); -} - fn log_context_errors(userdata: ?*anyopaque, error_message: [*c]const u8) callconv(.C) void { std.debug.assert(userdata == null); std.log.err("{s}", .{error_message}); } -fn reflect_image_samplers(compiler: ext.spvc_compiler, resources: ext.spvc_resources, stage: *sokol.gfx.ShaderStageDesc) Error!void { - var reflected_sampled_images: []const ext.spvc_reflected_resource = &.{}; +fn reflect_image_samplers(compiler: ext.spvc_compiler, resources: ext.spvc_resources, stage_desc: *sokol.gfx.ShaderStageDesc) Error!void { + var sampled_images: []const ext.spvc_reflected_resource = &.{}; try to_error(ext.spvc_resources_get_resource_list_for_type( resources, ext.SPVC_RESOURCE_TYPE_SAMPLED_IMAGE, - @ptrCast(&reflected_sampled_images.ptr), - &reflected_sampled_images.len, + @ptrCast(&sampled_images.ptr), + &sampled_images.len, )); - if (reflected_sampled_images.len > stage.image_sampler_pairs.len) { + if (sampled_images.len > stage_desc.image_sampler_pairs.len) { return error.UnsupportedSPIRV; } - for (0 .. reflected_sampled_images.len, reflected_sampled_images) |i, reflected_sampled_image| { - const sampled_image_type = ext.spvc_compiler_get_type_handle(compiler, reflected_sampled_image.type_id); + for (0 .. sampled_images.len, sampled_images) |i, sampled_image| { + const sampled_image_type = ext.spvc_compiler_get_type_handle(compiler, sampled_image.type_id); if (ext.spvc_type_get_basetype(sampled_image_type) != ext.SPVC_BASETYPE_SAMPLED_IMAGE) { return error.InvalidSPIRV; } - stage.images[i] = .{ + stage_desc.images[i] = .{ .multisampled = ext.spvc_type_get_image_multisampled(sampled_image_type) != 0, .image_type = try switch (ext.spvc_type_get_image_dimension(sampled_image_type)) { @@ -197,13 +196,13 @@ fn reflect_image_samplers(compiler: ext.spvc_compiler, resources: ext.spvc_resou .used = true, }; - stage.samplers[i] = .{ + stage_desc.samplers[i] = .{ .sampler_type = .DEFAULT, .used = true, }; - stage.image_sampler_pairs[i] = .{ - .glsl_name = ext.spvc_compiler_get_name(compiler, reflected_sampled_image.id), + stage_desc.image_sampler_pairs[i] = .{ + .glsl_name = ext.spvc_compiler_get_name(compiler, sampled_image.id), .image_slot = @intCast(i), .sampler_slot = @intCast(i), .used = true, @@ -211,85 +210,56 @@ fn reflect_image_samplers(compiler: ext.spvc_compiler, resources: ext.spvc_resou } } -fn reflect_uniform_blocks(compiler: ext.spvc_compiler, resources: ext.spvc_resources, stage: *sokol.gfx.ShaderStageDesc) Error!void { - var reflected_uniform_buffers: []const ext.spvc_reflected_resource = &.{}; +fn reflect_storage_buffers(compiler: ext.spvc_compiler, resources: ext.spvc_resources, stage_desc: *sokol.gfx.ShaderStageDesc) Error!void { + var storage_buffers: []const ext.spvc_reflected_resource = &.{}; + + try to_error(ext.spvc_resources_get_resource_list_for_type( + resources, + ext.SPVC_RESOURCE_TYPE_STORAGE_BUFFER, + @ptrCast(&storage_buffers.ptr), + &storage_buffers.len, + )); + + for (storage_buffers) |storage_buffer| { + const binding = ext.spvc_compiler_get_decoration(compiler, storage_buffer.id, ext.SpvDecorationBinding); + + if (binding >= stage_desc.storage_buffers.len) { + return error.InvalidSPIRV; + } + + var block_decorations: []const ext.SpvDecoration = &.{}; + + try to_error(ext.spvc_compiler_get_buffer_block_decorations( + compiler, + storage_buffer.id, + @ptrCast(&block_decorations.ptr), + &block_decorations.len, + )); + + stage_desc.storage_buffers[binding] = .{ + .readonly = std.mem.indexOfScalar(ext.SpvDecoration, block_decorations, ext.SpvDecorationNonWritable) != null, + .used = true, + }; + } +} + +fn reflect_uniform_buffers(compiler: ext.spvc_compiler, resources: ext.spvc_resources, stage_desc: *sokol.gfx.ShaderStageDesc) Error!void { + var uniform_buffers: []const ext.spvc_reflected_resource = &.{}; try to_error(ext.spvc_resources_get_resource_list_for_type( resources, ext.SPVC_RESOURCE_TYPE_UNIFORM_BUFFER, - @ptrCast(&reflected_uniform_buffers.ptr), - &reflected_uniform_buffers.len, + @ptrCast(&uniform_buffers.ptr), + &uniform_buffers.len, )); - if (reflected_uniform_buffers.len > stage.uniform_blocks.len) { - return error.UnsupportedSPIRV; + if (uniform_buffers.len != 0) { + return error.InvalidSPIRV; } - for (stage.uniform_blocks[0 .. reflected_uniform_buffers.len], reflected_uniform_buffers) |*uniform_block, reflected_uniform_buffer| { - const uniform_buffer_type = ext.spvc_compiler_get_type_handle(compiler, reflected_uniform_buffer.type_id); - - if (ext.spvc_type_get_basetype(uniform_buffer_type) != ext.SPVC_BASETYPE_STRUCT) { - return error.InvalidSPIRV; - } - - const member_count = ext.spvc_type_get_num_member_types(uniform_buffer_type); - - if (member_count > uniform_block.uniforms.len) { - return error.UnsupportedSPIRV; - } - - try to_error(ext.spvc_compiler_get_declared_struct_size(compiler, uniform_buffer_type, &uniform_block.size)); - - var uniform_blocks_used: u32 = 0; - - while (uniform_blocks_used < member_count) : (uniform_blocks_used += 1) { - const member_type_id = ext.spvc_type_get_member_type(uniform_buffer_type, uniform_blocks_used); - const member_type = ext.spvc_compiler_get_type_handle(compiler, member_type_id); - - uniform_block.uniforms[uniform_blocks_used] = .{ - // .name = ext.spvc_compiler_get_member_name(compiler, ext.spvc_type_get_base_type_id(uniform_buffer_type), uniform_blocks_used), - - .array_count = @intCast(try switch (ext.spvc_type_get_num_array_dimensions(member_type)) { - 0 => 0, - - 1 => switch (ext.spvc_type_array_dimension_is_literal(member_type, 1) != 0) { - true => ext.spvc_type_get_array_dimension(member_type, 1), - false => error.InvalidSPIRV, - }, - - else => error.InvalidSPIRV, - }), - - .type = try switch (ext.spvc_type_get_basetype(member_type)) { - ext.SPVC_BASETYPE_INT32 => switch (ext.spvc_type_get_vector_size(member_type)) { - 1 => if (ext.spvc_type_get_columns(member_type) == 1) sokol.gfx.UniformType.INT else error.InvalidSPIRV, - 2 => if (ext.spvc_type_get_columns(member_type) == 1) sokol.gfx.UniformType.INT2 else error.InvalidSPIRV, - 3 => if (ext.spvc_type_get_columns(member_type) == 1) sokol.gfx.UniformType.INT3 else error.InvalidSPIRV, - 4 => if (ext.spvc_type_get_columns(member_type) == 1) sokol.gfx.UniformType.INT4 else error.InvalidSPIRV, - else => error.InvalidSPIRV, - }, - - ext.SPVC_BASETYPE_FP32 => switch (ext.spvc_type_get_vector_size(member_type)) { - 1 => if (ext.spvc_type_get_columns(member_type) == 1) sokol.gfx.UniformType.FLOAT else error.InvalidSPIRV, - 2 => if (ext.spvc_type_get_columns(member_type) == 1) sokol.gfx.UniformType.FLOAT2 else error.InvalidSPIRV, - 3 => if (ext.spvc_type_get_columns(member_type) == 1) sokol.gfx.UniformType.FLOAT3 else error.InvalidSPIRV, - - 4 => switch (ext.spvc_type_get_columns(member_type)) { - 1 => sokol.gfx.UniformType.FLOAT4, - 4 => sokol.gfx.UniformType.MAT4, - else => error.InvalidSPIRV, - }, - - else => error.InvalidSPIRV, - }, - - else => error.InvalidSPIRV, - }, - }; - - // uniform_block.uniforms[uniform_blocks_used].name = ext.spvc_compiler_get_member_name(compiler, ext.spvc_type_get_base_type_id(uniform_buffer_type), uniform_blocks_used); - } - } + // TODO: Support for older APIs? + _ = stage_desc; + _ = compiler; } fn to_error(result: ext.spvc_result) Error!void {