renderer-mvp/asset-pipeline #53

Merged
kayomn merged 6 commits from renderer-mvp/asset-pipeline into main 2024-06-23 04:37:40 +02:00
20 changed files with 1229 additions and 1095 deletions

2
.gitignore vendored
View File

@ -1,4 +1,4 @@
# Generated assets
/zig-cache
/.zig-cache
/zig-out
*.glsl.zig

View File

@ -10,8 +10,8 @@
},
.dependencies = .{
.sokol = .{
.url = "git+https://github.com/floooh/sokol-zig.git#796a3d3d54c22d20da9e91a9a9120d5423d1e700",
.hash = "12209a187e071d76af00c02865677d170f844376866d062b1b5f82e4ecbd750c4e18",
.url = "git+https://github.com/floooh/sokol-zig.git#7c25767e51aa06dd5fb0684e4a8f2200d182ef27",
.hash = "1220fa7f47fbaf2f1ed8c17fab2d23b6a85bcbbc4aa0b3802c90a3e8bf6fca1f8569",
},
},
}

View File

@ -32,7 +32,7 @@ pub fn create_event(self: *Self, label: []const u8) std.mem.Allocator.Error!Even
const index = self.event_systems.len();
try self.event_systems.push(systems);
try self.event_systems.push_grow(systems);
return @enumFromInt(index);
}

View File

@ -2,7 +2,7 @@ pub const ascii = @import("./ascii.zig");
pub const dag = @import("./dag.zig");
pub const debug = @import("./debug.zig");
pub const files = @import("./files.zig");
pub const hashes = @import("./hashes.zig");

View File

@ -21,7 +21,7 @@ pub fn Graph(comptime Payload: type) type {
pub fn append(self: *Self, payload: Payload) std.mem.Allocator.Error!Node {
const node = @as(Node, @enumFromInt(self.table.len()));
try self.table.push(.{
try self.table.push_grow(.{
.payload = payload,
.edges = .{.allocator = self.table.allocator},
});
@ -83,7 +83,7 @@ pub fn Graph(comptime Payload: type) type {
};
if (slices.index_of(edges.values, 0, edge_node) == null) {
try edges.push(edge_node);
try edges.push_grow(edge_node);
}
return true;

View File

@ -1,5 +0,0 @@
const std = @import("std");
pub fn assert_ok(error_union: anytype) @typeInfo(@TypeOf(error_union)).ErrorUnion.payload {
return error_union catch unreachable;
}

124
src/coral/files.zig Normal file
View File

@ -0,0 +1,124 @@
const builtin = @import("builtin");
const io = @import("./io.zig");
const std = @import("std");
pub const Error = error {
FileNotFound,
FileInaccessible,
};
pub const Stat = struct {
size: u64,
};
pub const Storage = struct {
userdata: *anyopaque,
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,
};
pub fn read_bytes(self: Storage, path: []const u8, offset: usize, output: []io.Byte) Error!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 {
const decoded = (try self.read_native(path, offset, Type)) orelse {
return null;
};
return switch (@typeInfo(Type)) {
.Struct => std.mem.byteSwapAllFields(Type, &decoded),
else => @byteSwap(decoded),
};
}
pub fn read_native(self: Storage, path: []const u8, offset: usize, comptime Type: type) Error!?Type {
var buffer = @as([@sizeOf(Type)]io.Byte, undefined);
if (try self.vtable.read(self.userdata, path, offset, &buffer) != buffer.len) {
return null;
}
return @as(*align(1) const Type, @ptrCast(&buffer)).*;
}
pub const read_little = switch (native_endian) {
.little => read_native,
.big => read_foreign,
};
pub const read_big = switch (native_endian) {
.little => read_foreign,
.big => read_native,
};
};
pub const bundle = init: {
const Bundle = struct {
fn full_path(path: []const u8) Error![4095:0]u8 {
var buffer = [_:0]u8{0} ** 4095;
_ = std.fs.cwd().realpath(path, &buffer) catch {
return error.FileInaccessible;
};
return buffer;
}
fn read(_: *anyopaque, path: []const u8, offset: usize, output: []io.Byte) Error!usize {
var file = std.fs.openFileAbsoluteZ(&(try full_path(path)), .{.mode = .read_only}) catch |open_error| {
return switch (open_error) {
error.FileNotFound => error.FileNotFound,
else => error.FileInaccessible,
};
};
defer file.close();
if (offset != 0) {
file.seekTo(offset) catch {
return error.FileInaccessible;
};
}
return file.read(output) catch error.FileInaccessible;
}
fn stat(_: *anyopaque, path: []const u8) Error!Stat {
const file_stat = get: {
var file = std.fs.openFileAbsoluteZ(&(try full_path(path)), .{.mode = .read_only}) catch |open_error| {
return switch (open_error) {
error.FileNotFound => error.FileNotFound,
else => error.FileInaccessible,
};
};
defer file.close();
break: get file.stat() catch {
return error.FileInaccessible;
};
};
return .{
.size = file_stat.size,
};
}
};
break: init Storage{
.userdata = undefined,
.vtable = &.{
.stat = Bundle.stat,
.read = Bundle.read,
},
};
};
const native_endian = builtin.cpu.arch.endian();

View File

@ -8,21 +8,10 @@ const std = @import("std");
pub const Byte = u8;
kayomn marked this conversation as resolved
Review

Unused addition.

Unused addition.
pub const Decoder = coral.io.Functor(coral.io.Error!void, &.{[]coral.Byte});
pub const Error = error {
UnavailableResource,
};
pub fn FixedBuffer(comptime len: usize, comptime default_value: anytype) type {
const Value = @TypeOf(default_value);
return struct {
filled: usize = 0,
values: [len]Value = [_]Value{default_value} ** len,
};
}
pub fn Functor(comptime Output: type, comptime input_types: []const type) type {
const InputTuple = std.meta.Tuple(input_types);
@ -119,20 +108,6 @@ pub fn Generator(comptime Output: type, comptime input_types: []const type) type
};
}
pub const NullWritable = struct {
written: usize = 0,
pub fn writer(self: *NullWritable) Writer {
return Writer.bind(NullWritable, self, write);
}
pub fn write(self: *NullWritable, buffer: []const u8) !usize {
self.written += buffer.len;
return buffer.len;
}
};
pub const PrintError = Error || error {
IncompleteWrite,
};
@ -141,8 +116,6 @@ pub const Reader = Generator(Error!usize, &.{[]coral.Byte});
pub const Writer = Generator(Error!usize, &.{[]const coral.Byte});
const native_endian = builtin.cpu.arch.endian();
pub fn alloc_read(input: coral.io.Reader, allocator: std.mem.Allocator) []coral.Byte {
const buffer = coral.Stack(coral.Byte){.allocator = allocator};
@ -153,20 +126,6 @@ pub fn alloc_read(input: coral.io.Reader, allocator: std.mem.Allocator) []coral.
return buffer.to_allocation(streamed);
}
pub fn are_equal(a: []const Byte, b: []const Byte) bool {
if (a.len != b.len) {
return false;
}
for (0 .. a.len) |i| {
if (a[i] != b[i]) {
return false;
}
}
return true;
}
pub const bits_per_byte = 8;
pub fn bytes_of(value: anytype) []const Byte {
@ -179,14 +138,6 @@ pub fn bytes_of(value: anytype) []const Byte {
};
}
pub fn ends_with(haystack: []const Byte, needle: []const Byte) bool {
if (needle.len > haystack.len) {
return false;
}
return are_equal(haystack[haystack.len - needle.len ..], needle);
}
pub fn print(writer: Writer, utf8: []const u8) PrintError!void {
if (try writer.yield(.{utf8}) != utf8.len) {
return error.IncompleteWrite;
@ -208,35 +159,6 @@ pub fn skip_n(input: Reader, distance: u64) Error!void {
}
}
pub fn read_foreign(input: Reader, comptime Type: type) Error!Type {
const decoded = try read_native(input, Type);
return switch (@typeInfo(input)) {
.Struct => std.mem.byteSwapAllFields(Type, &decoded),
else => @byteSwap(decoded),
};
}
pub fn read_native(input: Reader, comptime Type: type) Error!Type {
var buffer = @as([@sizeOf(Type)]coral.Byte, undefined);
if (try input.yield(.{&buffer}) != buffer.len) {
return error.UnavailableResource;
}
return @as(*align(1) const Type, @ptrCast(&buffer)).*;
}
pub const read_little = switch (native_endian) {
.little => read_native,
.big => read_foreign,
};
pub const read_big = switch (native_endian) {
.little => read_foreign,
.big => read_native,
};
pub fn slice_sentineled(comptime sen: anytype, ptr: [*:sen]const @TypeOf(sen)) [:sen]const @TypeOf(sen) {
var len = @as(usize, 0);

View File

@ -29,6 +29,10 @@ pub fn Sequential(comptime Value: type) type {
}
pub fn grow(self: *Self, additional: usize) std.mem.Allocator.Error!void {
if (additional == 0) {
return;
}
const grown_capacity = self.cap + additional;
const buffer = try self.allocator.alloc(Value, grown_capacity);
@ -87,7 +91,39 @@ pub fn Sequential(comptime Value: type) type {
return true;
}
pub fn push(self: *Self, value: Value) std.mem.Allocator.Error!void {
pub fn push(self: *Self, value: Value) bool {
if (self.values.len == self.cap) {
return false;
}
const offset_index = self.values.len;
self.values = self.values.ptr[0 .. self.values.len + 1];
self.values[offset_index] = value;
return true;
}
pub fn push_all(self: *Self, values: []const Value) bool {
const new_length = self.values.len + values.len;
if (new_length > self.cap) {
return false;
}
const offset_index = self.values.len;
self.values = self.values.ptr[0 .. new_length];
for (0 .. values.len) |index| {
self.values[offset_index + index] = values[index];
}
return true;
}
pub fn push_grow(self: *Self, value: Value) std.mem.Allocator.Error!void {
if (self.values.len == self.cap) {
try self.grow(@max(1, self.cap));
}
@ -99,27 +135,11 @@ pub fn Sequential(comptime Value: type) type {
self.values[offset_index] = value;
}
pub fn push_all(self: *Self, values: []const Value) std.mem.Allocator.Error!void {
const new_length = self.values.len + values.len;
if (new_length > self.cap) {
try self.grow(new_length);
}
const offset_index = self.values.len;
self.values = self.values.ptr[0 .. new_length];
for (0 .. values.len) |index| {
self.values[offset_index + index] = values[index];
}
}
pub fn push_many(self: *Self, n: usize, value: Value) std.mem.Allocator.Error!void {
const new_length = self.values.len + n;
if (new_length > self.cap) {
try self.grow(new_length);
return false;
}
const offset_index = self.values.len;
@ -129,6 +149,8 @@ pub fn Sequential(comptime Value: type) type {
for (0 .. n) |index| {
self.values[offset_index + index] = value;
}
return true;
}
pub fn resize(self: *Self, size: usize, default_value: Value) std.mem.Allocator.Error!void {
@ -237,7 +259,7 @@ pub fn Parallel(comptime Value: type) type {
return self.values.len;
}
pub fn push(self: *Self, value: Value) std.mem.Allocator.Error!void {
pub fn push_grow(self: *Self, value: Value) std.mem.Allocator.Error!void {
if (self.len() == self.cap) {
try self.grow(@max(1, self.cap));
}

View File

@ -51,7 +51,7 @@ pub const BindContext = struct {
const id = resource.type_id(Resource);
if (!self.accesses_resource(.read_write, id)) {
try self.systems.graph.get_ptr(self.node).?.resource_accesses.push(.{.read_write = id});
try self.systems.graph.get_ptr(self.node).?.resource_accesses.push_grow(.{.read_write = id});
}
const read_write_resource_nodes = lazily_create: {
@ -65,7 +65,7 @@ pub const BindContext = struct {
};
if (slices.index_of(read_write_resource_nodes.values, 0, self.node) == null) {
try read_write_resource_nodes.push(self.node);
try read_write_resource_nodes.push_grow(self.node);
}
return value;
@ -79,7 +79,7 @@ pub const BindContext = struct {
const id = resource.type_id(Resource);
if (!self.accesses_resource(.read_only, id)) {
try self.systems.graph.get_ptr(self.node).?.resource_accesses.push(.{.read_only = id});
try self.systems.graph.get_ptr(self.node).?.resource_accesses.push_grow(.{.read_only = id});
}
const read_only_resource_nodes = lazily_create: {
@ -93,7 +93,7 @@ pub const BindContext = struct {
};
if (slices.index_of(read_only_resource_nodes.values, 0, self.node) == null) {
try read_only_resource_nodes.push(self.node);
try read_only_resource_nodes.push_grow(self.node);
}
return value;
@ -291,7 +291,7 @@ pub const Schedule = struct {
try populate_bundle(bundle, graph, edge);
}
try bundle.push(node);
try bundle.push_grow(node);
}
fn sort(schedule: *Schedule) !void {
@ -304,7 +304,7 @@ pub const Schedule = struct {
continue;
}
try schedule.parallel_work_bundles.push(.{.allocator = heap.allocator});
try schedule.parallel_work_bundles.push_grow(.{.allocator = heap.allocator});
const bundle = schedule.parallel_work_bundles.get_ptr().?;
@ -328,8 +328,9 @@ pub const Schedule = struct {
.main => {
const extracted_work = work.values[index ..];
try schedule.blocking_work.push_all(extracted_work);
try schedule.blocking_work.grow(extracted_work.len);
std.debug.assert(schedule.blocking_work.push_all(extracted_work));
std.debug.assert(work.pop_many(extracted_work.len));
},
}
@ -488,7 +489,7 @@ pub const Schedule = struct {
});
}
try nodes.push(node);
try nodes.push_grow(node);
self.invalidate_work();
}

View File

@ -2,8 +2,6 @@ const ascii = @import("./ascii.zig");
const coral = @import("./coral.zig");
const debug = @import("./debug.zig");
const io = @import("./io.zig");
const std = @import("std");
@ -16,7 +14,7 @@ pub fn alloc_formatted(allocator: std.mem.Allocator, comptime format: []const u8
errdefer buffer.deinit();
debug.assert_try(print_formatted(buffer.writer(), format, args));
print_formatted(buffer.writer(), format, args) catch unreachable;
return buffer.to_allocation(formatted_len, 0);
}
@ -24,7 +22,7 @@ pub fn alloc_formatted(allocator: std.mem.Allocator, comptime format: []const u8
fn count_formatted(comptime format: []const u8, args: anytype) usize {
var count = io.defaultWritable{};
debug.assert_try(print_formatted(count.writer(), format, args));
print_formatted(count.writer(), format, args) catch unreachable;
return count.written;
}

View File

@ -6,6 +6,7 @@ const ona = @import("ona");
const Actors = struct {
instances: coral.stack.Sequential(ona.gfx.Point2D) = .{.allocator = coral.heap.allocator},
quad_mesh_2d: ona.gfx.Handle = .none,
body_texture: ona.gfx.Handle = .none,
};
@ -21,45 +22,29 @@ pub fn main() !void {
});
}
fn load(display: coral.ReadBlocking(ona.gfx.Display), actors: coral.Write(Actors), gfx: ona.gfx.Work) !void {
display.res.resize(1280, 720);
fn load(display: coral.Write(ona.gfx.Display), actors: coral.Write(Actors), assets: coral.Write(ona.gfx.Assets)) !void {
display.res.width, display.res.height = .{1280, 720};
actors.res.body_texture = try assets.res.open_file(coral.files.bundle, "actor.bmp");
actors.res.quad_mesh_2d = try assets.res.open_quad_mesh_2d(@splat(1));
const crap = [_]u32{
0xFFFFFFFF, 0xFF000000, 0xFFFFFFFF, 0xFF000000,
0xFF000000, 0xFFFFFFFF, 0xFF000000, 0xFFFFFFFF,
0xFFFFFFFF, 0xFF000000, 0xFFFFFFFF, 0xFF000000,
0xFF000000, 0xFFFFFFFF, 0xFF000000, 0xFFFFFFFF,
};
actors.res.body_texture = try gfx.queue.open(.{
.resource = .{
.texture = .{
.data = coral.io.bytes_of(&crap),
.width = 4,
.access = .static,
.format = .bgra8888,
},
},
});
try actors.res.instances.push(.{0, 0});
try actors.res.instances.push_grow(.{0, 0});
}
fn exit(actors: coral.Write(Actors)) void {
actors.res.instances.deinit();
}
fn render(gfx: ona.gfx.Work, actors: coral.Write(Actors)) !void {
fn render(queue: ona.gfx.Queue, actors: coral.Write(Actors)) !void {
for (actors.res.instances.values) |instance| {
try gfx.queue.draw(.{
try queue.commands.append(.{
.instance_2d = .{
.mesh_2d = gfx.primitives.quad_mesh,
.mesh_2d = actors.res.quad_mesh_2d,
.texture = actors.res.body_texture,
.transform = .{
.origin = instance,
.xbasis = .{100, 0},
.ybasis = .{0, 100},
.xbasis = .{64, 0},
.ybasis = .{0, 64},
},
},
});

View File

@ -1,37 +1,112 @@
const App = @import("./App.zig");
pub const Queue = @import("./gfx/Queue.zig");
const coral = @import("coral");
const Device = @import("./gfx/Device.zig");
const device = @import("./gfx/device.zig");
const ext = @import("./ext.zig");
const formats = @import("./gfx/formats.zig");
const msg = @import("./msg.zig");
const std = @import("std");
pub const Assets = struct {
context: device.Context,
formats: coral.stack.Sequential(Format),
staging_arena: std.heap.ArenaAllocator,
pub const Format = struct {
extension: []const u8,
file_desc: *const fn (*std.heap.ArenaAllocator, coral.files.Storage, []const u8) Error!Desc,
pub const Error = std.mem.Allocator.Error || coral.files.Error || error {
FormatUnsupported,
};
};
pub fn open_file(self: *Assets, storage: coral.files.Storage, path: []const u8) (OpenError || Format.Error)!Handle {
kayomn marked this conversation as resolved Outdated

Wants to accept the desired storage source as an argument for loading resources from non-bundled locations like user storage or caller-defined storage implementations.

Wants to accept the desired storage source as an argument for loading resources from non-bundled locations like user storage or caller-defined storage implementations.
defer {
const max_cache_size = 536870912;
if (!self.staging_arena.reset(.{.retain_with_limit = max_cache_size})) {
std.log.warn("failed to retain staging arena size of {} bytes", .{max_cache_size});
}
}
for (self.formats.values) |format| {
if (!std.mem.endsWith(u8, path, format.extension)) {
continue;
}
return self.context.open(try format.file_desc(&self.staging_arena, storage, path));
}
return .none;
}
pub fn open_quad_mesh_2d(self: *Assets, extents: Point2D) OpenError!Handle {
const width, const height = extents / @as(Point2D, @splat(2));
return self.context.open(.{
.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 Color = @Vector(4, f32);
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 {
data: []const coral.io.Byte,
width: u16,
format: Format,
access: Access,
pub const Access = enum {
static,
};
pub const Format = enum {
rgba8,
bgra8,
pub fn byte_size(self: Format) usize {
return switch (self) {
.rgba8, .bgra8 => 4,
};
}
};
};
};
pub const Display = struct {
sdl_window: *ext.SDL_Window,
device: Device,
pub fn resize(self: Display, width: u16, height: u16) void {
ext.SDL_SetWindowSize(self.sdl_window, width, height);
}
pub fn retitle(self: Display, title: []const u8) void {
var sentineled_title = [_:0]u8{0} ** 255;
@memcpy(sentineled_title[0 .. @min(title.len, sentineled_title.len)], title);
ext.SDL_SetWindowTitle(self.sdl_window, &sentineled_title);
}
pub fn set_resizable(self: Display, resizable: bool) void {
ext.SDL_SetWindowResizable(self.sdl_window, @intFromBool(resizable));
}
width: u16 = 1280,
height: u16 = 720,
clear_color: Color = colors.black,
};
pub const Handle = enum (usize) {
@ -62,71 +137,68 @@ 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(_: coral.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 Transform2D = extern struct {
xbasis: Point2D = .{1, 0},
ybasis: Point2D = .{0, 1},
origin: Point2D = @splat(0),
};
pub const Work = struct {
queue: *Queue.Buffer,
primitives: *const Primitives,
const Primitives = struct {
quad_mesh: Handle,
};
pub const State = struct {
queue: *Queue,
primitives: *const Primitives,
};
pub fn bind(context: coral.system.BindContext) std.mem.Allocator.Error!State {
const queue = try Queue.create();
return .{
.primitives = (try context.register_read_only_resource_access(.none, Primitives)) orelse create: {
const buffer = queue.pending();
const half_extent = 0.5;
try context.world.set_resource(.none, Primitives{
.quad_mesh = try buffer.open(.{
.label = "quad mesh primitive",
.resource = .{
.mesh_2d = .{
.indices = &.{0, 1, 2, 0, 2, 3},
.vertices = &.{
.{.xy = .{-half_extent, half_extent}, .uv = .{0, 1}},
.{.xy = .{half_extent, half_extent}, .uv = .{1, 1}},
.{.xy = .{half_extent, -half_extent}, .uv = .{1, 0}},
.{.xy = .{-half_extent, -half_extent}, .uv = .{0, 0}},
},
},
},
}),
});
break: create (try context.register_read_only_resource_access(.none, Primitives)).?;
},
.queue = queue,
};
}
pub fn init(state: *State) Work {
return .{
.queue = state.queue.pending(),
.primitives = state.primitives,
};
}
pub fn unbind(state: *State) void {
state.queue.release();
}
const builtin_formats = [_]Assets.Format{
.{
.extension = "bmp",
.file_desc = formats.bmp_file_desc,
},
};
pub const colors = struct {
@ -156,43 +228,45 @@ pub fn poll(app: coral.Write(App), inputs: msg.Send(Input)) !void {
}
}
pub fn setup(world: *coral.World, events: App.Events) (error {SDLError} || std.Thread.SpawnError || std.mem.Allocator.Error)!void {
pub fn setup(world: *coral.World, events: App.Events) (error {Unsupported} || std.Thread.SpawnError || std.mem.Allocator.Error)!void {
if (ext.SDL_Init(ext.SDL_INIT_VIDEO) != 0) {
return error.SDLError;
return error.Unsupported;
}
const sdl_window = create: {
const position = ext.SDL_WINDOWPOS_CENTERED;
const flags = ext.SDL_WINDOW_OPENGL;
const width = 640;
const height = 480;
var context = try device.Context.init();
break: create ext.SDL_CreateWindow("Ona", position, position, width, height, flags) orelse {
return error.SDLError;
};
};
errdefer context.deinit();
errdefer ext.SDL_DestroyWindow(sdl_window);
var registered_formats = coral.stack.Sequential(Assets.Format){.allocator = coral.heap.allocator};
var device = try Device.init(sdl_window);
errdefer registered_formats.deinit();
errdefer device.deinit();
try registered_formats.grow(builtin_formats.len);
std.debug.assert(registered_formats.push_all(&builtin_formats));
try world.set_resource(.main, Display{
.device = device,
.sdl_window = sdl_window,
try world.set_resource(.none, Assets{
.staging_arena = std.heap.ArenaAllocator.init(coral.heap.allocator),
.formats = registered_formats,
.context = context,
});
try world.set_resource(.none, Display{});
try world.on_event(events.pre_update, coral.system_fn(poll), .{.label = "poll gfx"});
try world.on_event(events.exit, coral.system_fn(stop), .{.label = "stop gfx"});
try world.on_event(events.finish, coral.system_fn(submit), .{.label = "submit gfx"});
try world.on_event(events.finish, coral.system_fn(synchronize), .{.label = "synchronize gfx"});
}
pub fn stop(display: coral.WriteBlocking(Display)) void {
display.res.device.deinit();
ext.SDL_DestroyWindow(display.res.sdl_window);
pub fn stop(assets: coral.Write(Assets)) void {
assets.res.staging_arena.deinit();
assets.res.formats.deinit();
assets.res.context.deinit();
}
pub fn submit(display: coral.WriteBlocking(Display)) void {
display.res.device.submit();
pub fn synchronize(assets: coral.Write(Assets), display: coral.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,
});
}

View File

@ -1,534 +0,0 @@
const Queue = @import("./Queue.zig");
const coral = @import("coral");
const ext = @import("../ext.zig");
const gfx = @import("../gfx.zig");
const sokol = @import("sokol");
const std = @import("std");
thread: std.Thread,
clear_color: gfx.Color = gfx.colors.black,
state: *State,
const AtomicBool = std.atomic.Value(bool);
const Frame = struct {
width: u16,
height: u16,
flushed_instance_2d_count: usize = 0,
pushed_instance_2d_count: usize = 0,
mesh_2d: gfx.Handle = .none,
texture: gfx.Handle = .none,
fn unflushed_instance_2d_count(self: Frame) usize {
return self.pushed_instance_2d_count - self.flushed_instance_2d_count;
}
};
const Render = struct {
resources: coral.stack.Sequential(Resource),
instance_2d_pipeline: sokol.gfx.Pipeline,
instance_2d_buffers: coral.stack.Sequential(sokol.gfx.Buffer),
const Instance2D = 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,
};
const instances_per_buffer = 512;
const shader = @import("./shaders/instance_2d.glsl.zig");
};
const Resource = union (enum) {
empty,
mesh_2d: Mesh2D,
texture: Texture,
const Mesh2D = struct {
index_count: u32,
vertex_buffer: sokol.gfx.Buffer,
index_buffer: sokol.gfx.Buffer,
};
const Texture = struct {
image: sokol.gfx.Image,
sampler: sokol.gfx.Sampler,
};
};
fn deinit(self: *Render) void {
for (self.instance_2d_buffers.values) |buffer| {
sokol.gfx.destroyBuffer(buffer);
}
self.instance_2d_buffers.deinit();
sokol.gfx.destroyPipeline(self.instance_2d_pipeline);
self.resources.deinit();
}
fn init() Render {
sokol.gfx.setup(.{
.environment = .{
.defaults = .{
.color_format = .RGBA8,
.depth_format = .DEPTH_STENCIL,
.sample_count = 1,
},
},
.logger = .{
.func = sokol.log.func,
},
});
return .{
.instance_2d_pipeline = sokol.gfx.makePipeline(.{
.label = "2D drawing pipeline",
.layout = .{
.attrs = get: {
var attrs = [_]sokol.gfx.VertexAttrState{.{}} ** 16;
attrs[Instance2D.shader.ATTR_vs_mesh_xy] = .{
.format = .FLOAT2,
.buffer_index = Instance2D.buffer_indices.mesh,
};
attrs[Instance2D.shader.ATTR_vs_mesh_uv] = .{
.format = .FLOAT2,
.buffer_index = Instance2D.buffer_indices.mesh,
};
attrs[Instance2D.shader.ATTR_vs_instance_xbasis] = .{
.format = .FLOAT2,
.buffer_index = Instance2D.buffer_indices.instance,
};
attrs[Instance2D.shader.ATTR_vs_instance_ybasis] = .{
.format = .FLOAT2,
.buffer_index = Instance2D.buffer_indices.instance,
};
attrs[Instance2D.shader.ATTR_vs_instance_origin] = .{
.format = .FLOAT2,
.buffer_index = Instance2D.buffer_indices.instance,
};
attrs[Instance2D.shader.ATTR_vs_instance_tint] = .{
.format = .UBYTE4N,
.buffer_index = Instance2D.buffer_indices.instance,
};
attrs[Instance2D.shader.ATTR_vs_instance_depth] = .{
.format = .FLOAT,
.buffer_index = Instance2D.buffer_indices.instance,
};
attrs[Instance2D.shader.ATTR_vs_instance_rect] = .{
.format = .FLOAT4,
.buffer_index = Instance2D.buffer_indices.instance,
};
break: get attrs;
},
.buffers = get: {
var buffers = [_]sokol.gfx.VertexBufferLayoutState{.{}} ** 8;
buffers[Instance2D.buffer_indices.instance].step_func = .PER_INSTANCE;
break: get buffers;
},
},
.shader = sokol.gfx.makeShader(Instance2D.shader.draw2dShaderDesc(sokol.gfx.queryBackend())),
.index_type = .UINT16,
}),
.instance_2d_buffers = .{.allocator = coral.heap.allocator},
.resources = .{.allocator = coral.heap.allocator},
};
}
fn insert_resource(self: *Render, handle: gfx.Handle, resource: Resource) !void {
const handle_index = handle.index() orelse {
return error.InvalidHandle;
};
const resource_count = self.resources.len();
if (handle_index < resource_count) {
const empty_resource = &self.resources.values[handle_index];
if (empty_resource.* != .empty) {
return error.InvalidHandle;
}
empty_resource.* = resource;
} else {
if (handle_index != resource_count) {
return error.InvalidIndex;
}
try self.resources.push(resource);
}
}
fn flush_instance_2ds(self: *Render, frame: *Frame) void {
const unflushed_count = frame.unflushed_instance_2d_count();
if (unflushed_count == 0) {
return;
}
sokol.gfx.applyPipeline(self.instance_2d_pipeline);
sokol.gfx.applyUniforms(.VS, Instance2D.shader.SLOT_Screen, sokol.gfx.asRange(&Instance2D.shader.Screen{
.screen_size = .{@floatFromInt(frame.width), @floatFromInt(frame.height)},
}));
const mesh_2d = self.resources.values[frame.mesh_2d.index().?].mesh_2d;
const texture = self.resources.values[frame.texture.index().?].texture;
var bindings = sokol.gfx.Bindings{
.vertex_buffers = get: {
var buffers = [_]sokol.gfx.Buffer{.{}} ** 8;
buffers[Instance2D.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;
},
},
};
while (frame.flushed_instance_2d_count < frame.pushed_instance_2d_count) {
const buffer_index = frame.flushed_instance_2d_count / Instance2D.instances_per_buffer;
const buffer_offset = frame.flushed_instance_2d_count % Instance2D.instances_per_buffer;
const instances_to_flush = @min(Instance2D.instances_per_buffer - buffer_offset, unflushed_count);
bindings.vertex_buffers[Instance2D.buffer_indices.instance] = self.instance_2d_buffers.values[buffer_index];
bindings.vertex_buffer_offsets[Instance2D.buffer_indices.instance] = @intCast(buffer_offset);
sokol.gfx.applyBindings(bindings);
sokol.gfx.draw(0, mesh_2d.index_count, @intCast(instances_to_flush));
frame.flushed_instance_2d_count += instances_to_flush;
}
}
fn push_instance_2d(self: *Render, frame: *Frame, command: Queue.DrawCommand.Instance) std.mem.Allocator.Error!void {
if (command.mesh_2d != frame.mesh_2d or command.texture != frame.texture) {
self.flush_instance_2ds(frame);
}
frame.mesh_2d = command.mesh_2d;
frame.texture = command.texture;
const has_filled_buffer = (frame.pushed_instance_2d_count % Instance2D.instances_per_buffer) == 0;
const pushed_buffer_count = frame.pushed_instance_2d_count / Instance2D.instances_per_buffer;
if (has_filled_buffer and pushed_buffer_count == self.instance_2d_buffers.len()) {
const instance_buffer = sokol.gfx.makeBuffer(.{
.size = @sizeOf(Instance2D) * Instance2D.instances_per_buffer,
.usage = .STREAM,
.label = "2D drawing instance buffer",
});
errdefer sokol.gfx.destroyBuffer(instance_buffer);
try self.instance_2d_buffers.push(instance_buffer);
}
_ = sokol.gfx.appendBuffer(self.instance_2d_buffers.get().?, sokol.gfx.asRange(&Instance2D{
.transform = command.transform,
}));
frame.pushed_instance_2d_count += 1;
}
fn remove_resource(self: *Render, handle: gfx.Handle) ?Resource {
if (handle.index()) |handle_index| {
const resource = self.resources.values[handle_index];
if (resource != .empty) {
self.resources.values[handle_index] = .empty;
return resource;
}
}
return null;
}
};
const Self = @This();
const State = struct {
finished: std.Thread.Semaphore = .{},
is_running: AtomicBool = AtomicBool.init(true),
ready: std.Thread.Semaphore = .{},
clear_color: gfx.Color = gfx.colors.black,
};
pub fn deinit(self: *Self) void {
self.state.is_running.store(false, .monotonic);
self.state.ready.post();
self.thread.join();
coral.heap.allocator.destroy(self.state);
self.* = undefined;
}
pub fn init(window: *ext.SDL_Window) (std.mem.Allocator.Error || std.Thread.SpawnError)!Self {
const state = try coral.heap.allocator.create(State);
errdefer coral.heap.allocator.destroy(state);
state.* = .{};
const thread = try std.Thread.spawn(.{}, run, .{window, state});
thread.setName("Ona Graphics") catch {
std.log.warn("failed to name the graphics thread", .{});
};
return .{
.thread = thread,
.state = state,
};
}
fn process_close_command(command: Queue.CloseCommand, rendering: *Render) !void {
const resource = &rendering.resources.values[command.handle.index().?];
switch (resource.*) {
.empty => {}, // TODO: Handle this.
.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);
},
}
resource.* = .empty;
}
fn process_draw_command(command: Queue.DrawCommand, render: *Render, frame: *Frame) !void {
switch (command) {
.instance_2d => |instance_2d| {
try render.push_instance_2d(frame, instance_2d);
},
.post_process => |post_process| {
render.flush_instance_2ds(frame);
// sokol.gfx.applyPipeline(self.post_process_pipeline);
_ = post_process;
},
}
render.flush_instance_2ds(frame);
}
fn process_open_command(command: Queue.OpenCommand, render: *Render) !void {
switch (command.resource) {
.texture => |texture| {
const stride = texture.width * texture.format.byte_size();
const image = sokol.gfx.makeImage(.{
.width = texture.width,
.height = @intCast(texture.data.len / stride),
.data = .{
.subimage = get: {
var subimage = [_][16]sokol.gfx.Range{.{.{}} ** 16} ** 6;
subimage[0][0] = sokol.gfx.asRange(texture.data);
break: get subimage;
},
},
});
errdefer sokol.gfx.destroyImage(image);
const sampler = sokol.gfx.makeSampler(.{});
errdefer sokol.gfx.destroySampler(sampler);
try render.insert_resource(command.handle, .{
.texture = .{
.sampler = sampler,
.image = image,
},
});
},
.mesh_2d => |mesh_2d| {
const index_buffer = sokol.gfx.makeBuffer(.{
.data = sokol.gfx.asRange(mesh_2d.indices),
.type = .INDEXBUFFER,
});
const vertex_buffer = sokol.gfx.makeBuffer(.{
.data = sokol.gfx.asRange(mesh_2d.vertices),
.type = .VERTEXBUFFER,
});
errdefer {
sokol.gfx.destroyBuffer(index_buffer);
sokol.gfx.destroyBuffer(vertex_buffer);
}
if (mesh_2d.indices.len > std.math.maxInt(u32)) {
return error.OutOfMemory;
}
try render.insert_resource(command.handle, .{
.mesh_2d = .{
.index_buffer = index_buffer,
.vertex_buffer = vertex_buffer,
.index_count = @intCast(mesh_2d.indices.len),
},
});
},
}
}
fn run(window: *ext.SDL_Window, state: *State) !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 render = Render.init();
defer render.deinit();
state.finished.post();
while (state.is_running.load(.monotonic)) {
state.ready.wait();
defer state.finished.post();
var frame = init_frame: {
var width, var height = [_]c_int{0, 0};
ext.SDL_GL_GetDrawableSize(window, &width, &height);
std.debug.assert(width > 0 and height > 0);
break: init_frame Frame{
.width = @intCast(width),
.height = @intCast(height),
};
};
sokol.gfx.beginPass(.{
.swapchain = .{
.width = frame.width,
.height = frame.height,
.sample_count = 1,
.color_format = .RGBA8,
.depth_format = .DEPTH_STENCIL,
.gl = .{.framebuffer = 0},
},
.action = .{
.colors = get: {
var actions = [_]sokol.gfx.ColorAttachmentAction{.{}} ** 4;
actions[0] = .{
.load_action = .CLEAR,
.clear_value = @as(sokol.gfx.Color, @bitCast(state.clear_color)),
};
break: get actions;
},
},
});
try Queue.visit_open_commands(process_open_command, .{&render});
try Queue.visit_draw_commands(process_draw_command, .{&render, &frame});
try Queue.visit_close_commands(process_close_command, .{&render});
sokol.gfx.endPass();
sokol.gfx.commit();
ext.SDL_GL_SwapWindow(window);
}
}
pub fn submit(self: *Self) void {
self.state.finished.wait();
self.state.clear_color = self.clear_color;
Queue.swap();
self.state.ready.post();
}

View File

@ -1,309 +0,0 @@
const coral = @import("coral");
const gfx = @import("../gfx.zig");
const std = @import("std");
buffers: [2]Buffer,
is_swapped: bool = false,
ref_count: AtomicCount = AtomicCount.init(1),
has_next: ?*Self = null,
has_prev: ?*Self = null,
const AtomicCount = std.atomic.Value(usize);
pub const Buffer = struct {
arena: std.heap.ArenaAllocator,
closed_handles: coral.stack.Sequential(usize),
open_commands: coral.stack.Sequential(OpenCommand),
draw_commands: coral.stack.Sequential(DrawCommand),
close_commands: coral.stack.Sequential(CloseCommand),
pub fn clear(self: *Buffer) void {
self.close_commands.clear();
self.draw_commands.clear();
self.open_commands.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: *Buffer) void {
self.arena.deinit();
self.closed_handles.deinit();
self.open_commands.deinit();
self.draw_commands.deinit();
self.close_commands.deinit();
}
fn init(allocator: std.mem.Allocator) Buffer {
return .{
.arena = std.heap.ArenaAllocator.init(allocator),
.closed_handles = .{.allocator = allocator},
.open_commands = .{.allocator = allocator},
.draw_commands = .{.allocator = allocator},
.close_commands = .{.allocator = allocator},
};
}
pub fn draw(self: *Buffer, command: DrawCommand) std.mem.Allocator.Error!void {
try self.draw_commands.push(switch (command) {
.instance_2d => |instance_2d| .{.instance_2d = instance_2d},
.post_process => |post_process| .{.post_process = post_process},
});
}
pub fn open(self: *Buffer, command: OpenCommand) std.mem.Allocator.Error!gfx.Handle {
const reserved_handle = @as(gfx.Handle, switch (command.handle) {
.none => @enumFromInt(reserve_handle: {
if (self.closed_handles.get()) |handle| {
std.debug.assert(self.closed_handles.pop());
break: reserve_handle handle;
}
break: reserve_handle next_handle.fetchAdd(1, .monotonic);
}),
_ => |handle| handle,
});
std.debug.assert(reserved_handle != .none);
const arena_allocator = self.arena.allocator();
try self.open_commands.push(.{
.resource = switch (command.resource) {
.texture => |texture| .{
.texture = .{
.data = try arena_allocator.dupe(coral.io.Byte, texture.data),
.width = texture.width,
.format = texture.format,
.access = texture.access,
},
},
.mesh_2d => |mesh_2d| .{
.mesh_2d = .{
.indices = try arena_allocator.dupe(u16, mesh_2d.indices),
.vertices = try arena_allocator.dupe(OpenCommand.Mesh2D.Vertex, mesh_2d.vertices),
},
},
},
.label = if (command.label) |label| try arena_allocator.dupe(coral.io.Byte, label) else null,
.handle = reserved_handle,
});
return reserved_handle;
}
};
pub const CloseCommand = struct {
handle: gfx.Handle,
};
pub const DrawCommand = union (enum) {
instance_2d: Instance,
post_process: PostProcess,
pub const Instance = struct {
texture: gfx.Handle,
mesh_2d: gfx.Handle,
transform: gfx.Transform2D,
};
pub const PostProcess = struct {
};
};
pub const OpenCommand = struct {
handle: gfx.Handle = .none,
label: ?[]const u8 = null,
resource: union (enum) {
texture: Texture,
mesh_2d: Mesh2D,
},
pub const Mesh2D = struct {
vertices: []const Vertex,
indices: []const u16,
pub const Vertex = struct {
xy: gfx.Point2D,
uv: gfx.Point2D,
};
};
pub const Texture = struct {
data: []const coral.io.Byte,
width: u16,
format: Format,
access: Access,
pub const Access = enum {
static,
};
pub const Format = enum {
rgba8888,
bgra8888,
argb8888,
rgb888,
bgr888,
pub fn byte_size(self: Format) usize {
return switch (self) {
.rgba8888, .bgra8888, .argb8888 => 4,
.rgb888, .bgr888 => 3,
};
}
};
};
};
pub fn acquire(self: *Self) void {
self.ref_count.fetchAdd(1, .monotonic);
}
pub fn create() std.mem.Allocator.Error!*Self {
const queue = try coral.heap.allocator.create(Self);
errdefer coral.heap.allocator.destroy(queue);
queue.* = .{
.buffers = .{Buffer.init(coral.heap.allocator), Buffer.init(coral.heap.allocator)},
};
mutex.lock();
defer mutex.unlock();
if (has_tail) |tail| {
tail.has_next = queue;
queue.has_prev = tail;
} else {
std.debug.assert(has_head == null);
has_head = queue;
}
has_tail = queue;
return queue;
}
pub fn pending(self: *Self) *Buffer {
return &self.buffers[@intFromBool(self.is_swapped)];
}
pub fn release(self: *Self) void {
if (self.ref_count.fetchSub(1, .monotonic) == 1) {
mutex.lock();
defer mutex.unlock();
if (self.has_prev) |prev| {
prev.has_next = self.has_next;
} else {
has_head = self.has_next;
}
if (self.has_next) |next| {
next.has_prev = self.has_prev;
} else {
has_tail = self.has_prev;
}
for (&self.buffers) |*buffer| {
buffer.deinit();
}
coral.heap.allocator.destroy(self);
}
}
pub fn submitted(self: *Self) *Buffer {
return &self.buffers[@intFromBool(!self.is_swapped)];
}
const Self = @This();
var has_head = @as(?*Self, null);
var has_tail = @as(?*Self, null);
var mutex = std.Thread.Mutex{};
var next_handle = AtomicCount.init(1);
pub fn swap() void {
mutex.lock();
defer mutex.unlock();
var has_node = has_head;
while (has_node) |node| : (has_node = node.has_next) {
node.is_swapped = !node.is_swapped;
node.pending().clear();
}
}
pub fn visit_close_commands(visit: anytype, args: anytype) !void {
mutex.lock();
defer mutex.unlock();
var has_node = has_head;
var iterations = @as(usize, 0);
while (has_node) |node| : ({
has_node = node.has_next;
iterations += 1;
}) {
for (node.submitted().close_commands.values) |command| {
try @call(.auto, visit, .{command} ++ args);
}
}
}
pub fn visit_draw_commands(visit: anytype, args: anytype) !void {
mutex.lock();
defer mutex.unlock();
var has_node = has_head;
var iterations = @as(usize, 0);
while (has_node) |node| : ({
has_node = node.has_next;
iterations += 1;
}) {
for (node.submitted().draw_commands.values) |command| {
try @call(.auto, visit, .{command} ++ args);
}
}
}
pub fn visit_open_commands(visit: anytype, args: anytype) !void {
mutex.lock();
defer mutex.unlock();
var has_node = has_head;
var iterations = @as(usize, 0);
while (has_node) |node| : ({
has_node = node.has_next;
iterations += 1;
}) {
for (node.submitted().open_commands.values) |command| {
try @call(.auto, visit, .{command} ++ args);
}
}
}

80
src/ona/gfx/commands.zig Normal file
View File

@ -0,0 +1,80 @@
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},
};
}
};
}

691
src/ona/gfx/device.zig Normal file
View File

@ -0,0 +1,691 @@
const commands = @import("./commands.zig");
const coral = @import("coral");
const ext = @import("../ext.zig");
const gfx = @import("../gfx.zig");
const sokol = @import("sokol");
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 handle_index = handle.index() orelse {
return;
};
const close_commands = self.loop.closes.pending();
std.debug.assert(close_commands.stack.cap > close_commands.stack.len());
close_commands.append(.{.index = handle_index}) catch unreachable;
}
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 open(self: *Context, desc: gfx.Desc) gfx.OpenError!gfx.Handle {
const open_commands = self.loop.opens.pending();
const index = self.loop.closed_indices.get() orelse open_commands.stack.len();
try open_commands.append(.{
.index = index,
.desc = desc,
});
const pending_closes = self.loop.closes.pending();
if (pending_closes.stack.len() == pending_closes.stack.cap) {
try pending_closes.stack.grow(1);
}
_ = self.loop.closed_indices.pop();
return @enumFromInt(index + 1);
}
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.opens.swap();
self.loop.closes.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 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 = &.{},
closes: CloseChain = CloseChain.init(coral.heap.allocator),
opens: OpenChain = OpenChain.init(coral.heap.allocator),
closed_indices: coral.stack.Sequential(usize) = .{.allocator = coral.heap.allocator},
const AtomicBool = std.atomic.Value(bool);
const CloseCommand = struct {
index: usize,
};
const OpenCommand = struct {
index: usize,
desc: gfx.Desc,
fn clone(command: OpenCommand, arena: *std.heap.ArenaAllocator) std.mem.Allocator.Error!OpenCommand {
const allocator = arena.allocator();
return .{
.desc = switch (command.desc) {
.texture => |texture| .{
.texture = .{
.data = try allocator.dupe(coral.io.Byte, texture.data),
.width = texture.width,
.format = texture.format,
.access = texture.access,
},
},
.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),
},
},
},
.index = command.index,
};
}
};
const CloseChain = commands.Chain(CloseCommand, null);
const OpenChain = commands.Chain(OpenCommand, OpenCommand.clone);
fn deinit(self: *Loop) void {
self.closes.deinit();
self.opens.deinit();
self.closed_indices.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 = Rendering.init();
defer rendering.deinit();
self.finished.post();
while (self.is_running.load(.monotonic)) {
self.ready.wait();
defer self.finished.post();
const open_commands = self.opens.submitted();
defer open_commands.clear();
for (open_commands.stack.values) |command| {
switch (command.desc) {
.texture => |texture| {
const stride = texture.width * texture.format.byte_size();
const image = sokol.gfx.makeImage(.{
.width = texture.width,
.height = @intCast(texture.data.len / stride),
.data = .{
.subimage = get: {
var subimage = [_][16]sokol.gfx.Range{.{.{}} ** 16} ** 6;
subimage[0][0] = sokol.gfx.asRange(texture.data);
break: get subimage;
},
},
.pixel_format = switch (texture.format) {
.rgba8 => .RGBA8,
.bgra8 => .BGRA8,
},
});
errdefer sokol.gfx.destroyImage(image);
const sampler = sokol.gfx.makeSampler(.{});
errdefer sokol.gfx.destroySampler(sampler);
try rendering.insert_object(command.index, .{
.texture = .{
.sampler = sampler,
.image = image,
},
});
},
.mesh_2d => |mesh_2d| {
const index_buffer = sokol.gfx.makeBuffer(.{
.data = sokol.gfx.asRange(mesh_2d.indices),
.type = .INDEXBUFFER,
});
const vertex_buffer = sokol.gfx.makeBuffer(.{
.data = sokol.gfx.asRange(mesh_2d.vertices),
.type = .VERTEXBUFFER,
});
errdefer {
sokol.gfx.destroyBuffer(index_buffer);
sokol.gfx.destroyBuffer(vertex_buffer);
}
if (mesh_2d.indices.len > std.math.maxInt(u32)) {
return error.OutOfMemory;
}
try rendering.insert_object(command.index, .{
.mesh_2d = .{
.index_buffer = index_buffer,
.vertex_buffer = vertex_buffer,
.index_count = @intCast(mesh_2d.indices.len),
},
});
},
}
}
var frame = init_frame: {
var width, var height = [_]c_int{0, 0};
ext.SDL_GL_GetDrawableSize(window, &width, &height);
std.debug.assert(width > 0 and height > 0);
break: init_frame Rendering.Frame{
.width = @intCast(width),
.height = @intCast(height),
};
};
sokol.gfx.beginPass(.{
.swapchain = .{
.width = frame.width,
.height = frame.height,
.sample_count = 1,
.color_format = .RGBA8,
.depth_format = .DEPTH_STENCIL,
.gl = .{.framebuffer = 0},
},
.action = .{
.colors = get: {
var actions = [_]sokol.gfx.ColorAttachmentAction{.{}} ** 4;
actions[0] = .{
.load_action = .CLEAR,
.clear_value = @as(sokol.gfx.Color, @bitCast(self.clear_color)),
};
break: get actions;
},
},
});
for (self.renders) |*render| {
const render_commands = render.submitted();
defer render_commands.clear();
for (render_commands.stack.values) |command| {
switch (command) {
.instance_2d => |instance_2d| {
try rendering.push_instance_2d(&frame, instance_2d);
},
.post_process => |post_process| {
rendering.flush_instance_2ds(&frame);
// sokol.gfx.applyPipeline(self.post_process_pipeline);
_ = post_process;
},
}
}
}
rendering.flush_instance_2ds(&frame);
sokol.gfx.endPass();
sokol.gfx.commit();
ext.SDL_GL_SwapWindow(window);
const close_commands = self.closes.submitted();
defer close_commands.clear();
for (close_commands.stack.values) |command| {
const object = &rendering.objects.values[command.index];
switch (object.*) {
.empty => {}, // TODO: Handle double-closes.
.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);
},
}
object.* = .empty;
}
}
}
};
const Rendering = struct {
objects: coral.stack.Sequential(Object),
instance_2d_pipeline: sokol.gfx.Pipeline,
instance_2d_buffers: coral.stack.Sequential(sokol.gfx.Buffer),
const Instance2D = 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,
};
const instances_per_buffer = 512;
const shader = @import("./shaders/instance_2d.glsl.zig");
};
const Frame = struct {
width: u16,
height: u16,
flushed_instance_2d_count: usize = 0,
pushed_instance_2d_count: usize = 0,
mesh_2d: gfx.Handle = .none,
texture: gfx.Handle = .none,
fn unflushed_instance_2d_count(self: Frame) usize {
return self.pushed_instance_2d_count - self.flushed_instance_2d_count;
}
};
const Object = union (enum) {
empty,
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,
},
};
fn deinit(self: *Rendering) void {
for (self.instance_2d_buffers.values) |buffer| {
sokol.gfx.destroyBuffer(buffer);
}
self.instance_2d_buffers.deinit();
sokol.gfx.destroyPipeline(self.instance_2d_pipeline);
self.objects.deinit();
}
fn init() Rendering {
sokol.gfx.setup(.{
.environment = .{
.defaults = .{
.color_format = .RGBA8,
.depth_format = .DEPTH_STENCIL,
.sample_count = 1,
},
},
.logger = .{
.func = sokol.log.func,
},
});
return .{
.instance_2d_pipeline = sokol.gfx.makePipeline(.{
.label = "2D drawing pipeline",
.layout = .{
.attrs = get: {
var attrs = [_]sokol.gfx.VertexAttrState{.{}} ** 16;
attrs[Instance2D.shader.ATTR_vs_mesh_xy] = .{
.format = .FLOAT2,
.buffer_index = Instance2D.buffer_indices.mesh,
};
attrs[Instance2D.shader.ATTR_vs_mesh_uv] = .{
.format = .FLOAT2,
.buffer_index = Instance2D.buffer_indices.mesh,
};
attrs[Instance2D.shader.ATTR_vs_instance_xbasis] = .{
.format = .FLOAT2,
.buffer_index = Instance2D.buffer_indices.instance,
};
attrs[Instance2D.shader.ATTR_vs_instance_ybasis] = .{
.format = .FLOAT2,
.buffer_index = Instance2D.buffer_indices.instance,
};
attrs[Instance2D.shader.ATTR_vs_instance_origin] = .{
.format = .FLOAT2,
.buffer_index = Instance2D.buffer_indices.instance,
};
attrs[Instance2D.shader.ATTR_vs_instance_tint] = .{
.format = .UBYTE4N,
.buffer_index = Instance2D.buffer_indices.instance,
};
attrs[Instance2D.shader.ATTR_vs_instance_depth] = .{
.format = .FLOAT,
.buffer_index = Instance2D.buffer_indices.instance,
};
attrs[Instance2D.shader.ATTR_vs_instance_rect] = .{
.format = .FLOAT4,
.buffer_index = Instance2D.buffer_indices.instance,
};
break: get attrs;
},
.buffers = get: {
var buffers = [_]sokol.gfx.VertexBufferLayoutState{.{}} ** 8;
buffers[Instance2D.buffer_indices.instance].step_func = .PER_INSTANCE;
break: get buffers;
},
},
.shader = sokol.gfx.makeShader(Instance2D.shader.draw2dShaderDesc(sokol.gfx.queryBackend())),
.index_type = .UINT16,
}),
.instance_2d_buffers = .{.allocator = coral.heap.allocator},
.objects = .{.allocator = coral.heap.allocator},
};
}
fn flush_instance_2ds(self: *Rendering, frame: *Frame) void {
const unflushed_count = frame.unflushed_instance_2d_count();
if (unflushed_count == 0) {
return;
}
sokol.gfx.applyPipeline(self.instance_2d_pipeline);
sokol.gfx.applyUniforms(.VS, Instance2D.shader.SLOT_Screen, sokol.gfx.asRange(&Instance2D.shader.Screen{
.screen_size = .{@floatFromInt(frame.width), @floatFromInt(frame.height)},
}));
const mesh_2d = self.objects.values[frame.mesh_2d.index().?].mesh_2d;
const texture = self.objects.values[frame.texture.index().?].texture;
var bindings = sokol.gfx.Bindings{
.vertex_buffers = get: {
var buffers = [_]sokol.gfx.Buffer{.{}} ** 8;
buffers[Instance2D.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;
},
},
};
while (frame.flushed_instance_2d_count < frame.pushed_instance_2d_count) {
const buffer_index = frame.flushed_instance_2d_count / Instance2D.instances_per_buffer;
const buffer_offset = frame.flushed_instance_2d_count % Instance2D.instances_per_buffer;
const instances_to_flush = @min(Instance2D.instances_per_buffer - buffer_offset, unflushed_count);
bindings.vertex_buffers[Instance2D.buffer_indices.instance] = self.instance_2d_buffers.values[buffer_index];
bindings.vertex_buffer_offsets[Instance2D.buffer_indices.instance] = @intCast(buffer_offset);
sokol.gfx.applyBindings(bindings);
sokol.gfx.draw(0, mesh_2d.index_count, @intCast(instances_to_flush));
frame.flushed_instance_2d_count += instances_to_flush;
}
}
fn insert_object(self: *Rendering, index: usize, object: Object) !void {
const resource_count = self.objects.len();
if (index < resource_count) {
const empty_object = &self.objects.values[index];
if (empty_object.* != .empty) {
return error.InvalidHandle;
}
empty_object.* = object;
} else {
if (index != resource_count) {
return error.InvalidIndex;
}
try self.objects.push_grow(object);
}
}
fn push_instance_2d(self: *Rendering, frame: *Frame, command: RenderCommand.Instance) std.mem.Allocator.Error!void {
if (command.mesh_2d != frame.mesh_2d or command.texture != frame.texture) {
self.flush_instance_2ds(frame);
}
frame.mesh_2d = command.mesh_2d;
frame.texture = command.texture;
const has_filled_buffer = (frame.pushed_instance_2d_count % Instance2D.instances_per_buffer) == 0;
const pushed_buffer_count = frame.pushed_instance_2d_count / Instance2D.instances_per_buffer;
if (has_filled_buffer and pushed_buffer_count == self.instance_2d_buffers.len()) {
const instance_buffer = sokol.gfx.makeBuffer(.{
.size = @sizeOf(Instance2D) * Instance2D.instances_per_buffer,
.usage = .STREAM,
.label = "2D drawing instance buffer",
});
errdefer sokol.gfx.destroyBuffer(instance_buffer);
try self.instance_2d_buffers.push_grow(instance_buffer);
}
_ = sokol.gfx.appendBuffer(self.instance_2d_buffers.get().?, sokol.gfx.asRange(&Instance2D{
.transform = command.transform,
}));
frame.pushed_instance_2d_count += 1;
}
fn remove_object(self: *Rendering, index: usize) ?Object {
const object = self.objects.values[index];
if (object != .empty) {
self.objects.values[index] = .empty;
return object;
}
return null;
}
};
pub const RenderCommand = union (enum) {
instance_2d: Instance,
post_process: PostProcess,
pub const Instance = struct {
texture: gfx.Handle,
mesh_2d: gfx.Handle,
transform: gfx.Transform2D,
};
pub const PostProcess = struct {
};
fn clone(self: RenderCommand, arena: *std.heap.ArenaAllocator) std.mem.Allocator.Error!RenderCommand {
_ = arena;
return switch (self) {
.instance_2d => |instance_2d| .{.instance_2d = instance_2d},
.post_process => |post_process| .{.post_process = post_process},
};
}
};
pub const RenderChain = commands.Chain(RenderCommand, RenderCommand.clone);
pub const RenderList = commands.List(RenderCommand, RenderCommand.clone);

81
src/ona/gfx/formats.zig Normal file
View File

@ -0,0 +1,81 @@
const coral = @import("coral");
const gfx = @import("../gfx.zig");
const std = @import("std");
pub fn bmp_file_desc(
arena: *std.heap.ArenaAllocator,
storage: coral.files.Storage,
path: []const u8,
) gfx.Assets.Format.Error!gfx.Desc {
const header = try storage.read_little(path, 0, extern struct {
type: [2]u8 align (1),
file_size: u32 align (1),
reserved: [2]u16 align (1),
image_offset: u32 align (1),
header_size: u32 align (1),
pixel_width: i32 align (1),
pixel_height: i32 align (1),
color_planes: u16 align (1),
bits_per_pixel: u16 align (1),
compression_method: u32 align (1),
image_size: u32 align(1),
pixels_per_meter_x: i32 align (1),
pixels_per_meter_y: i32 align (1),
palette_colors_used: u32 align (1),
important_colors_used: u32 align (1),
}) orelse {
return error.FormatUnsupported;
};
if (!std.mem.eql(u8, &header.type, "BM")) {
return error.FormatUnsupported;
}
const pixel_width = std.math.cast(u16, header.pixel_width) orelse {
return error.FormatUnsupported;
};
const pixels = try arena.allocator().alloc(coral.io.Byte, header.image_size);
const bytes_per_pixel = header.bits_per_pixel / coral.io.bits_per_byte;
const alignment = 4;
const byte_stride = pixel_width * bytes_per_pixel;
const padded_byte_stride = alignment * @divTrunc((byte_stride + alignment - 1), alignment);
const byte_padding = coral.scalars.sub(padded_byte_stride, byte_stride) orelse 0;
var buffer_offset: usize = 0;
var file_offset = @as(usize, header.image_offset);
switch (header.bits_per_pixel) {
32 => {
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) {
return error.FormatUnsupported;
}
for (0 .. pixel_width) |i| {
const line_offset = i * 4;
const pixel = line[line_offset .. line_offset + 4];
std.mem.swap(u8, &pixel[0], &pixel[2]);
}
file_offset += line.len + byte_padding;
buffer_offset += padded_byte_stride;
}
},
else => return error.FormatUnsupported,
}
return .{
.texture = .{
.width = pixel_width,
.data = pixels,
.format = .rgba8,
.access = .static,
}
};
}

View File

@ -51,6 +51,10 @@ out vec4 texel;
void main() {
texel = texture(sampler2D(tex, smp), uv) * color;
if (texel.a == 0) {
discard;
}
}
@end

View File

@ -7,7 +7,7 @@ const std = @import("std");
fn Channel(comptime Message: type) type {
return struct {
buffers: [2]coral.stack.Sequential(Message),
swapped: bool = false,
swap_index: u1 = 0,
ticks: u1 = 0,
const Self = @This();
@ -28,9 +28,9 @@ fn Channel(comptime Message: type) type {
channel.res.ticks = coral.scalars.add(channel.res.ticks, 1) orelse 0;
if (channel.res.ticks == 0) {
channel.res.swapped = !channel.res.swapped;
channel.res.swap_index ^= 1;
channel.res.buffers[@intFromBool(channel.res.swapped)].clear();
channel.res.buffers[channel.res.swap_index].clear();
}
}
@ -44,11 +44,11 @@ fn Channel(comptime Message: type) type {
}
fn messages(self: Self) []const Message {
return self.buffers[@intFromBool(!self.swapped)].values;
return self.buffers[self.swap_index ^ 1].values;
}
fn push(self: *Self, message: Message) std.mem.Allocator.Error!void {
try self.buffers[@intFromBool(self.swapped)].push(message);
try self.buffers[self.swap_index].push_grow(message);
}
};
}