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
16 changed files with 993 additions and 1016 deletions
Showing only changes of commit 04e8d69c37 - Show all commits

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,8 +2,6 @@ pub const ascii = @import("./ascii.zig");
pub const dag = @import("./dag.zig");
pub const debug = @import("./debug.zig");
pub const hashes = @import("./hashes.zig");
pub const heap = @import("./heap.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;
}

View File

@ -53,7 +53,7 @@ pub const Table = struct {
errdefer resource_allocator.destroy(allocated_resource);
std.debug.assert(try self.table.emplace(value_id, .{
.ptr = allocated_resource,
.ptr = @ptrCast(allocated_resource),
kayomn marked this conversation as resolved Outdated

Bad idea to forcibly convert non-trivial downcasts like **opaque actually

Bad idea to forcibly convert non-trivial downcasts like `**opaque` actually
}));
allocated_resource.* = value;

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

@ -21,45 +21,28 @@ 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("actor.bmp");
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), assets: coral.Read(ona.gfx.Assets)) !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 = assets.res.primitives.quad_mesh,
.texture = actors.res.body_texture,
.transform = .{
.origin = instance,
.xbasis = .{100, 0},
.ybasis = .{0, 100},
.xbasis = .{64, 0},
.ybasis = .{0, 64},
},
},
});

View File

@ -1,10 +1,10 @@
const App = @import("./App.zig");
pub const Queue = @import("./gfx/Queue.zig");
const coral = @import("coral");
const Device = @import("./gfx/Device.zig");
const formats = @import("./gfx/formats.zig");
const device = @import("./gfx/device.zig");
const ext = @import("./ext.zig");
@ -12,26 +12,47 @@ const msg = @import("./msg.zig");
const std = @import("std");
pub const Assets = struct {
context: device.Context,
primitives: Primitives,
formats: coral.stack.Sequential(Format),
pub const Format = struct {
extension: []const u8,
open: *const fn ([]const u8) std.mem.Allocator.Error!Handle,
};
const Primitives = struct {
quad_mesh: Handle,
};
pub fn close(self: *Assets, handle: Handle) void {
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.
return self.context.close(handle);
}
pub fn open_file(self: *Assets, path: []const u8) std.mem.Allocator.Error!Handle {
for (self.formats.values) |format| {
if (!std.mem.endsWith(u8, path, format.extension)) {
continue;
}
return format.open(path);
}
return .none;
}
pub fn open_mesh_2d(self: *Assets, mesh_2d: Mesh2D) std.mem.Allocator.Error!Handle {
return self.context.open(.{.mesh_2d = mesh_2d});
}
};
pub const Color = @Vector(4, f32);
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) {
@ -64,69 +85,86 @@ pub const Input = union (enum) {
pub const Point2D = @Vector(2, f32);
pub const Mesh2D = struct {
vertices: []const Vertex,
indices: []const u16,
pub const Vertex = struct {
xy: Point2D,
uv: Point2D,
};
};
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.
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());
std.mem.swap(device.RenderChain, &renders.values[state.command_index], renders.get_ptr().?);
std.debug.assert(renders.pop());
}
var renders = coral.stack.Sequential(device.RenderChain){.allocator = coral.heap.allocator};
};
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 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",
.open = formats.open_bmp,
},
};
pub const colors = struct {
@ -156,43 +194,59 @@ 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,
const half_extent = 0.5;
try world.set_resource(.none, Assets{
.primitives = .{
.quad_mesh = try context.open(.{
.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}},
},
},
}),
},
.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.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},
};
}
};
}

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

@ -0,0 +1,680 @@
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 Asset = union (enum) {
texture: gfx.Texture,
mesh_2d: gfx.Mesh2D,
};
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();
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, asset: Asset) std.mem.Allocator.Error!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,
.payload = asset,
});
try self.loop.closes.pending().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,
payload: Asset,
fn clone(command: OpenCommand, arena: *std.heap.ArenaAllocator) std.mem.Allocator.Error!OpenCommand {
const allocator = arena.allocator();
return .{
.payload = switch (command.payload) {
.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.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 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.payload) {
.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 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);

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

@ -0,0 +1,9 @@
const gfx = @import("../gfx.zig");
const std = @import("std");
pub fn open_bmp(path: []const u8) std.mem.Allocator.Error!gfx.Handle {
// TODO: Implement.
_ = path;
unreachable;
}

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