diff --git a/src/main.zig b/src/main.zig index 9462235..0512d73 100644 --- a/src/main.zig +++ b/src/main.zig @@ -5,8 +5,8 @@ const std = @import("std"); const ona = @import("ona"); const Actors = struct { - instances: coral.stack.Sequential(ona.gfx.Queue.Instance2D) = .{.allocator = coral.heap.allocator}, - body_texture: ona.gfx.Queue.Handle = .none, + instances: coral.stack.Sequential(ona.gfx.Point2D) = .{.allocator = coral.heap.allocator}, + body_texture: ona.gfx.Handle = .none, }; const Player = struct { @@ -21,15 +21,9 @@ pub fn main() !void { }); } -fn load(display: coral.ReadBlocking(ona.gfx.Display), actors: coral.Write(Actors), gfx: ona.gfx.Queue) !void { +fn load(display: coral.ReadBlocking(ona.gfx.Display), actors: coral.Write(Actors), gfx: ona.gfx.Work) !void { display.res.resize(1280, 720); - try actors.res.instances.push_many(800, .{ - .origin = .{75, 75}, - .xbasis = .{100, 0}, - .ybasis = .{0, 100}, - }); - const crap = [_]u32{ 0xFFFFFFFF, 0xFF000000, 0xFFFFFFFF, 0xFF000000, 0xFF000000, 0xFFFFFFFF, 0xFF000000, 0xFFFFFFFF, @@ -37,7 +31,7 @@ fn load(display: coral.ReadBlocking(ona.gfx.Display), actors: coral.Write(Actors 0xFF000000, 0xFFFFFFFF, 0xFF000000, 0xFFFFFFFF, }; - actors.res.body_texture = try gfx.buffer.open(.{ + actors.res.body_texture = try gfx.queue.open(.{ .resource = .{ .texture = .{ .data = coral.io.bytes_of(&crap), @@ -47,22 +41,33 @@ fn load(display: coral.ReadBlocking(ona.gfx.Display), actors: coral.Write(Actors }, }, }); + + try actors.res.instances.push(.{0, 0}); } fn exit(actors: coral.Write(Actors)) void { actors.res.instances.deinit(); } -fn render(gfx: ona.gfx.Queue, actors: coral.Write(Actors)) !void { - try gfx.buffer.draw_2d(.{ - .mesh_2d = gfx.primitives.quad_mesh, - .instances = actors.res.instances.values, - .texture = actors.res.body_texture, - }); +fn render(gfx: ona.gfx.Work, actors: coral.Write(Actors)) !void { + for (actors.res.instances.values) |instance| { + try gfx.queue.draw(.{ + .instance_2d = .{ + .mesh_2d = gfx.primitives.quad_mesh, + .texture = actors.res.body_texture, + + .transform = .{ + .origin = instance, + .xbasis = .{100, 0}, + .ybasis = .{0, 100}, + }, + }, + }); + } } fn update(player: coral.Read(Player), actors: coral.Write(Actors), mapping: coral.Read(ona.act.Mapping)) !void { - actors.res.instances.values[0].origin += .{ + actors.res.instances.values[0] += .{ mapping.res.axis_strength(player.res.move_x), mapping.res.axis_strength(player.res.move_y), }; diff --git a/src/ona/gfx.zig b/src/ona/gfx.zig index 6806472..f934cd2 100644 --- a/src/ona/gfx.zig +++ b/src/ona/gfx.zig @@ -1,22 +1,21 @@ const App = @import("./App.zig"); -const Device = @import("./gfx/Device.zig"); - pub const Queue = @import("./gfx/Queue.zig"); -pub const color = @import("./gfx/color.zig"); - const coral = @import("coral"); +const Device = @import("./gfx/Device.zig"); + const ext = @import("./ext.zig"); const msg = @import("./msg.zig"); const std = @import("std"); +pub const Color = @Vector(4, f32); + pub const Display = struct { sdl_window: *ext.SDL_Window, - clear_color: color.Value = color.black, device: Device, pub fn resize(self: Display, width: u16, height: u16) void { @@ -35,11 +34,17 @@ pub const Display = struct { } }; -pub const Error = error { - SDLError, -}; +pub const Handle = enum (usize) { + none, + _, -pub const Handle = Queue.Handle; + pub fn index(self: Handle) ?usize { + return switch (self) { + .none => null, + _ => @intFromEnum(self) - 1, + }; + } +}; pub const Input = union (enum) { key_up: Key, @@ -57,8 +62,85 @@ pub const Input = union (enum) { }; }; -pub const MeshPrimitives = struct { - quad_mesh: Handle, +pub const Point2D = @Vector(2, f32); + +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(); + } +}; + +pub const colors = struct { + pub const black = greyscale(0); + + pub const white = greyscale(1); + + pub fn greyscale(v: f32) Color { + return .{v, v, v, 1}; + } + + pub fn rgb(r: f32, g: f32, b: f32) Color { + return .{r, g, b, 1}; + } }; pub fn poll(app: coral.Write(App), inputs: msg.Send(Input)) !void { @@ -74,7 +156,7 @@ pub fn poll(app: coral.Write(App), inputs: msg.Send(Input)) !void { } } -pub fn setup(world: *coral.World, events: App.Events) (Error || std.Thread.SpawnError || std.mem.Allocator.Error)!void { +pub fn setup(world: *coral.World, events: App.Events) (error {SDLError} || std.Thread.SpawnError || std.mem.Allocator.Error)!void { if (ext.SDL_Init(ext.SDL_INIT_VIDEO) != 0) { return error.SDLError; } @@ -112,5 +194,5 @@ pub fn stop(display: coral.WriteBlocking(Display)) void { } pub fn submit(display: coral.WriteBlocking(Display)) void { - display.res.device.submit(display.res.sdl_window, display.res.clear_color); + display.res.device.submit(); } diff --git a/src/ona/gfx/Device.zig b/src/ona/gfx/Device.zig index 1bef10c..a604e7d 100644 --- a/src/ona/gfx/Device.zig +++ b/src/ona/gfx/Device.zig @@ -1,75 +1,100 @@ const Queue = @import("./Queue.zig"); -const color = @import("./color.zig"); - const coral = @import("coral"); const draw_2d = @import("./shaders/draw_2d.glsl.zig"); const ext = @import("../ext.zig"); +const gfx = @import("../gfx.zig"); + const sokol = @import("sokol"); const std = @import("std"); thread: std.Thread, -render_state: *RenderState, +clear_color: gfx.Color = gfx.colors.black, +state: *State, const AtomicBool = std.atomic.Value(bool); -const RenderWork = struct { - pipeline_2d: sokol.gfx.Pipeline, - instance_2d_sampler: sokol.gfx.Sampler, - instance_2d_buffers: coral.stack.Sequential(sokol.gfx.Buffer), +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 Resource = union (enum) { - mesh_2d: struct { + empty, + mesh_2d: Mesh2D, + texture: Texture, + + const Mesh2D = struct { index_count: u32, vertex_buffer: sokol.gfx.Buffer, index_buffer: sokol.gfx.Buffer, - }, + }; - texture: struct { + const Texture = struct { image: sokol.gfx.Image, - }, + sampler: sokol.gfx.Sampler, + }; }; - const buffer_indices = .{ - .mesh = 0, - .instance = 1, - }; - - fn deinit(self: *RenderWork) void { - sokol.gfx.destroyPipeline(self.pipeline_2d); - + fn deinit(self: *Render) void { for (self.instance_2d_buffers.values) |buffer| { sokol.gfx.destroyBuffer(buffer); } self.instance_2d_buffers.deinit(); - - for (self.resources.values) |resource| { - switch (resource) { - .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.destroyPipeline(self.instance_2d_pipeline); self.resources.deinit(); - - self.* = undefined; } - fn init(allocator: std.mem.Allocator) RenderWork { + fn init() Render { + sokol.gfx.setup(.{ + .environment = .{ + .defaults = .{ + .color_format = .RGBA8, + .depth_format = .DEPTH_STENCIL, + .sample_count = 1, + }, + }, + + .logger = .{ + .func = sokol.log.func, + }, + }); + return .{ - .pipeline_2d = sokol.gfx.makePipeline(.{ + .instance_2d_pipeline = sokol.gfx.makePipeline(.{ .label = "2D drawing pipeline", .layout = .{ @@ -78,42 +103,42 @@ const RenderWork = struct { attrs[draw_2d.ATTR_vs_mesh_xy] = .{ .format = .FLOAT2, - .buffer_index = buffer_indices.mesh, + .buffer_index = Instance2D.buffer_indices.mesh, }; attrs[draw_2d.ATTR_vs_mesh_uv] = .{ .format = .FLOAT2, - .buffer_index = buffer_indices.mesh, + .buffer_index = Instance2D.buffer_indices.mesh, }; attrs[draw_2d.ATTR_vs_instance_xbasis] = .{ .format = .FLOAT2, - .buffer_index = buffer_indices.instance, + .buffer_index = Instance2D.buffer_indices.instance, }; attrs[draw_2d.ATTR_vs_instance_ybasis] = .{ .format = .FLOAT2, - .buffer_index = buffer_indices.instance, + .buffer_index = Instance2D.buffer_indices.instance, }; attrs[draw_2d.ATTR_vs_instance_origin] = .{ .format = .FLOAT2, - .buffer_index = buffer_indices.instance, + .buffer_index = Instance2D.buffer_indices.instance, }; - attrs[draw_2d.ATTR_vs_instance_color] = .{ + attrs[draw_2d.ATTR_vs_instance_tint] = .{ .format = .UBYTE4N, - .buffer_index = buffer_indices.instance, + .buffer_index = Instance2D.buffer_indices.instance, }; attrs[draw_2d.ATTR_vs_instance_depth] = .{ .format = .FLOAT, - .buffer_index = buffer_indices.instance, + .buffer_index = Instance2D.buffer_indices.instance, }; attrs[draw_2d.ATTR_vs_instance_rect] = .{ .format = .FLOAT4, - .buffer_index = buffer_indices.instance, + .buffer_index = Instance2D.buffer_indices.instance, }; break: get attrs; @@ -122,7 +147,7 @@ const RenderWork = struct { .buffers = get: { var buffers = [_]sokol.gfx.VertexBufferLayoutState{.{}} ** 8; - buffers[buffer_indices.instance].step_func = .PER_INSTANCE; + buffers[Instance2D.buffer_indices.instance].step_func = .PER_INSTANCE; break: get buffers; }, @@ -132,273 +157,296 @@ const RenderWork = struct { .index_type = .UINT16, }), - .instance_2d_sampler = sokol.gfx.makeSampler(.{ - .label = "instance 2D sampler", - }), - .instance_2d_buffers = .{.allocator = coral.heap.allocator}, - .resources = .{.allocator = allocator}, + .resources = .{.allocator = coral.heap.allocator}, }; } - fn process_draw_2d_commands(self: *RenderWork, commands: []const Queue.Buffer.Draw2DCommand, target: Queue.Target) std.mem.Allocator.Error!void { - const max_instances = 512; - var instance_2d_buffers_used = @as(usize, 0); + fn insert_resource(self: *Render, handle: gfx.Handle, resource: Resource) !void { + const handle_index = handle.index() orelse { + return error.InvalidHandle; + }; - sokol.gfx.applyPipeline(self.pipeline_2d); + 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, draw_2d.SLOT_Screen, sokol.gfx.asRange(&draw_2d.Screen{ - .screen_size = .{target.width, target.height}, + .screen_size = .{@floatFromInt(frame.width), @floatFromInt(frame.height)}, })); - for (commands) |command| { - const mesh_2d = &self.resources.values[command.mesh_2d.index().?].mesh_2d; - const texture = &self.resources.values[command.texture.index().?].texture; - const instance_size = @sizeOf(Queue.Instance2D); - const full_instance_buffer_count = command.instances.len / max_instances; + const mesh_2d = self.resources.values[frame.mesh_2d.index().?].mesh_2d; + const texture = self.resources.values[frame.texture.index().?].texture; - for (0 .. full_instance_buffer_count) |i| { - defer instance_2d_buffers_used += 1; + var bindings = sokol.gfx.Bindings{ + .vertex_buffers = get: { + var buffers = [_]sokol.gfx.Buffer{.{}} ** 8; - if (instance_2d_buffers_used == self.instance_2d_buffers.len()) { - const instance_2d_buffer = sokol.gfx.makeBuffer(.{ - .size = @sizeOf(Queue.Instance2D) * max_instances, - .usage = .STREAM, - .label = "2D drawing instance buffer", - }); + buffers[Instance2D.buffer_indices.mesh] = mesh_2d.vertex_buffer; - errdefer sokol.gfx.destroyBuffer(instance_2d_buffer); + break: get buffers; + }, - try self.instance_2d_buffers.push(instance_2d_buffer); - } + .index_buffer = mesh_2d.index_buffer, - sokol.gfx.applyBindings(.{ - .vertex_buffers = get_buffers: { - var buffers = [_]sokol.gfx.Buffer{.{}} ** 8; + .fs = .{ + .images = get: { + var images = [_]sokol.gfx.Image{.{}} ** 12; - buffers[buffer_indices.instance] = self.instance_2d_buffers.values[instance_2d_buffers_used]; - buffers[buffer_indices.mesh] = mesh_2d.vertex_buffer; + images[0] = texture.image; - break: get_buffers 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] = self.instance_2d_sampler; - - break: get samplers; - }, - }, - }); - - sokol.gfx.updateBuffer(self.instance_2d_buffers.values[instance_2d_buffers_used], .{ - .ptr = command.instances.ptr + (max_instances * i), - .size = instance_size * max_instances, - }); - - sokol.gfx.draw(0, mesh_2d.index_count, max_instances); - } - - defer instance_2d_buffers_used += 1; - - if (instance_2d_buffers_used == self.instance_2d_buffers.len()) { - const instance_2d_buffer = sokol.gfx.makeBuffer(.{ - .size = @sizeOf(Queue.Instance2D) * max_instances, - .usage = .STREAM, - .label = "2D drawing instance buffer", - }); - - errdefer sokol.gfx.destroyBuffer(instance_2d_buffer); - - try self.instance_2d_buffers.push(instance_2d_buffer); - } - - sokol.gfx.applyBindings(.{ - .vertex_buffers = get_buffers: { - var buffers = [_]sokol.gfx.Buffer{.{}} ** 8; - - buffers[buffer_indices.instance] = self.instance_2d_buffers.values[instance_2d_buffers_used]; - buffers[buffer_indices.mesh] = mesh_2d.vertex_buffer; - - break: get_buffers buffers; + break: get images; }, - .fs = .{ - .images = get: { - var images = [_]sokol.gfx.Image{.{}} ** 12; + .samplers = get: { + var samplers = [_]sokol.gfx.Sampler{.{}} ** 8; - images[0] = texture.image; + samplers[0] = texture.sampler; - break: get images; - }, - - .samplers = get: { - var samplers = [_]sokol.gfx.Sampler{.{}} ** 8; - - samplers[0] = self.instance_2d_sampler; - - break: get samplers; - }, + break: get samplers; }, + }, + }; - .index_buffer = mesh_2d.index_buffer, - }); + 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); - const remaining_instances = command.instances.len % max_instances; + 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.updateBuffer(self.instance_2d_buffers.values[instance_2d_buffers_used], .{ - .ptr = command.instances.ptr + full_instance_buffer_count, - .size = instance_size * remaining_instances, - }); + sokol.gfx.applyBindings(bindings); + sokol.gfx.draw(0, mesh_2d.index_count, @intCast(instances_to_flush)); - sokol.gfx.draw(0, mesh_2d.index_count, @intCast(remaining_instances)); + frame.flushed_instance_2d_count += instances_to_flush; } } - fn process_open_commands(self: *RenderWork, commands: []const Queue.Buffer.OpenCommand) std.mem.Allocator.Error!void { - for (commands) |command| { - switch (command.resource) { - .texture => |texture| { - const stride = texture.width * texture.format.byte_size(); + 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); + } - const image = sokol.gfx.makeImage(.{ - .width = texture.width, - .height = @intCast(texture.data.len / stride), + frame.mesh_2d = command.mesh_2d; + frame.texture = command.texture; - .data = .{ - .subimage = get: { - var subimage = [_][16]sokol.gfx.Range{.{.{}} ** 16} ** 6; + 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; - subimage[0][0] = sokol.gfx.asRange(texture.data); + 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", + }); - break: get subimage; - }, - }, - }); + errdefer sokol.gfx.destroyBuffer(instance_buffer); - errdefer sokol.gfx.destroyImage(image); + try self.instance_2d_buffers.push(instance_buffer); + } - try self.resources.push(.{ - .texture = .{ - .image = image, - }, - }); - }, + _ = sokol.gfx.appendBuffer(self.instance_2d_buffers.get().?, sokol.gfx.asRange(&Instance2D{ + .transform = command.transform, + })); - .mesh_2d => |mesh_2d| { - const index_buffer = sokol.gfx.makeBuffer(.{ - .data = sokol.gfx.asRange(mesh_2d.indices), - .type = .INDEXBUFFER, - }); + frame.pushed_instance_2d_count += 1; + } - const vertex_buffer = sokol.gfx.makeBuffer(.{ - .data = sokol.gfx.asRange(mesh_2d.vertices), - .type = .VERTEXBUFFER, - }); + fn remove_resource(self: *Render, handle: gfx.Handle) ?Resource { + if (handle.index()) |handle_index| { + const resource = self.resources.values[handle_index]; - errdefer { - sokol.gfx.destroyBuffer(index_buffer); - sokol.gfx.destroyBuffer(vertex_buffer); - } + if (resource != .empty) { + self.resources.values[handle_index] = .empty; - if (mesh_2d.indices.len > std.math.maxInt(u32)) { - return error.OutOfMemory; - } - - try self.resources.push(.{ - .mesh_2d = .{ - .index_buffer = index_buffer, - .vertex_buffer = vertex_buffer, - .index_count = @intCast(mesh_2d.indices.len), - }, - }); - }, + return resource; } } - } - fn process_queue(self: *RenderWork, buffer: *const Queue.Buffer, target: Queue.Target) std.mem.Allocator.Error!void { - try self.process_open_commands(buffer.open_commands.values); - try self.process_draw_2d_commands(buffer.draw_2d_commands.values, target); + return null; } }; -const RenderState = struct { - finished: std.Thread.Semaphore = .{}, - is_running: AtomicBool = AtomicBool.init(true), - ready: std.Thread.Semaphore = .{}, - clear_color: color.Value = color.compress(color.black), - pixel_width: c_int = 0, - pixel_height: c_int = 0, -}; - 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.render_state.is_running.store(false, .monotonic); - self.render_state.ready.post(); + self.state.is_running.store(false, .monotonic); + self.state.ready.post(); self.thread.join(); - coral.heap.allocator.destroy(self.render_state); + coral.heap.allocator.destroy(self.state); self.* = undefined; } -pub fn init(sdl_window: *ext.SDL_Window) (std.mem.Allocator.Error || std.Thread.SpawnError)!Self { - const render_state = try coral.heap.allocator.create(RenderState); +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(render_state); + errdefer coral.heap.allocator.destroy(state); - render_state.* = .{}; + state.* = .{}; - const self = Self{ - .thread = spawn_thread: { - const thread = try std.Thread.spawn(.{}, run, .{sdl_window, render_state}); + const thread = try std.Thread.spawn(.{}, run, .{window, state}); - thread.setName("Ona Graphics") catch { - std.log.warn("failed to name the graphics thread", .{}); - }; - - break: spawn_thread thread; - }, - - .render_state = render_state, + thread.setName("Ona Graphics") catch { + std.log.warn("failed to name the graphics thread", .{}); }; - self.submit(sdl_window, .{0, 0, 0, 1}); - - return self; + return .{ + .thread = thread, + .state = state, + }; } -fn run(sdl_window: *ext.SDL_Window, render_state: *RenderState) !void { - var result = @as(c_int, 0); +fn process_close_command(command: Queue.CloseCommand, rendering: *Render) !void { + const resource = &rendering.resources.values[command.handle.index().?]; - 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); + switch (resource.*) { + .empty => {}, // TODO: Handle this. - if (result != 0) { - std.log.err("failed to set necessary OpenGL flags in graphics", .{}); + .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); + }, } - const context = ext.SDL_GL_CreateContext(sdl_window); + resource.* = .empty; +} - defer ext.SDL_GL_DeleteContext(context); +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); + }, - render_state.finished.post(); + .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 = .{ @@ -414,21 +462,38 @@ fn run(sdl_window: *ext.SDL_Window, render_state: *RenderState) !void { }, }); - defer sokol.gfx.shutdown(); + defer { + sokol.gfx.shutdown(); + ext.SDL_GL_DeleteContext(context); + } - var render_work = RenderWork.init(coral.heap.allocator); + var render = Render.init(); - defer render_work.deinit(); + defer render.deinit(); - while (render_state.is_running.load(.monotonic)) { - render_state.ready.wait(); + state.finished.post(); - defer render_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 = render_state.pixel_width, - .height = render_state.pixel_height, + .width = frame.width, + .height = frame.height, .sample_count = 1, .color_format = .RGBA8, .depth_format = .DEPTH_STENCIL, @@ -441,32 +506,29 @@ fn run(sdl_window: *ext.SDL_Window, render_state: *RenderState) !void { actions[0] = .{ .load_action = .CLEAR, - .clear_value = @as(sokol.gfx.Color, @bitCast(render_state.clear_color)), + .clear_value = @as(sokol.gfx.Color, @bitCast(state.clear_color)), }; break: get actions; }, - } + }, }); - try Queue.consume_submitted(Queue.Consumer.bind(RenderWork, &render_work, RenderWork.process_queue), .{ - .width = @floatFromInt(render_state.pixel_width), - .height = @floatFromInt(render_state.pixel_height), - }); + 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(sdl_window); + ext.SDL_GL_SwapWindow(window); } } -pub fn submit(self: Self, sdl_window: *ext.SDL_Window, clear_color: color.Value) void { - self.render_state.finished.wait(); - ext.SDL_GL_GetDrawableSize(sdl_window, &self.render_state.pixel_width, &self.render_state.pixel_height); - std.debug.assert(self.render_state.pixel_width > 0 and self.render_state.pixel_height > 0); +pub fn submit(self: *Self) void { + self.state.finished.wait(); - self.render_state.clear_color = clear_color; + self.state.clear_color = self.clear_color; Queue.swap(); - self.render_state.ready.post(); + self.state.ready.post(); } diff --git a/src/ona/gfx/Queue.zig b/src/ona/gfx/Queue.zig index 958cf22..76a2029 100644 --- a/src/ona/gfx/Queue.zig +++ b/src/ona/gfx/Queue.zig @@ -1,11 +1,14 @@ -const color = @import("./color.zig"); - const coral = @import("coral"); +const gfx = @import("../gfx.zig"); + const std = @import("std"); -buffer: *Buffer, -primitives: *const Primitives, +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); @@ -13,65 +16,24 @@ pub const Buffer = struct { arena: std.heap.ArenaAllocator, closed_handles: coral.stack.Sequential(usize), open_commands: coral.stack.Sequential(OpenCommand), - draw_2d_commands: coral.stack.Sequential(Draw2DCommand), + draw_commands: coral.stack.Sequential(DrawCommand), close_commands: coral.stack.Sequential(CloseCommand), - pub const CloseCommand = struct { - handle: Handle, - }; + pub fn clear(self: *Buffer) void { + self.close_commands.clear(); + self.draw_commands.clear(); + self.open_commands.clear(); - pub const Draw2DCommand = struct { - instances: []const Instance2D, - texture: Handle, - mesh_2d: Handle, - }; - - pub const OpenCommand = struct { - handle: Handle = .none, - label: ?[]const u8 = null, - - resource: union (enum) { - texture: Texture, - mesh_2d: Mesh2D, - }, - - pub const Mesh2D = struct { - vertices: []const Vertex2D, - indices: []const u16, - }; - - 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, - }; - } - }; - }; - }; + 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_2d_commands.deinit(); + self.draw_commands.deinit(); self.close_commands.deinit(); } @@ -80,31 +42,20 @@ pub const Buffer = struct { .arena = std.heap.ArenaAllocator.init(allocator), .closed_handles = .{.allocator = allocator}, .open_commands = .{.allocator = allocator}, - .draw_2d_commands = .{.allocator = allocator}, + .draw_commands = .{.allocator = allocator}, .close_commands = .{.allocator = allocator}, }; } - pub fn clear(self: *Buffer) void { - self.close_commands.clear(); - self.draw_2d_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", .{}); - } - } - - pub fn draw_2d(self: *Buffer, command: Draw2DCommand) std.mem.Allocator.Error!void { - try self.draw_2d_commands.push(.{ - .instances = try self.arena.allocator().dupe(Instance2D, command.instances), - .texture = command.texture, - .mesh_2d = command.mesh_2d, + 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!Handle { - const reserved_handle = @as(Handle, switch (command.handle) { + 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()); @@ -136,7 +87,7 @@ pub const Buffer = struct { .mesh_2d => |mesh_2d| .{ .mesh_2d = .{ .indices = try arena_allocator.dupe(u16, mesh_2d.indices), - .vertices = try arena_allocator.dupe(Vertex2D, mesh_2d.vertices), + .vertices = try arena_allocator.dupe(OpenCommand.Mesh2D.Vertex, mesh_2d.vertices), }, }, }, @@ -149,101 +100,77 @@ pub const Buffer = struct { } }; -pub const Consumer = coral.io.Generator(std.mem.Allocator.Error!void, &.{*const Buffer, Target}); +pub const CloseCommand = struct { + handle: gfx.Handle, +}; -pub const Handle = enum (usize) { - none, - _, +pub const DrawCommand = union (enum) { + instance_2d: Instance, + post_process: PostProcess, - pub fn index(self: Handle) ?usize { - return switch (self) { - .none => null, - _ => @intFromEnum(self) - 1, + 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 Instance2D = extern struct { - xbasis: Point2D = .{1, 0}, - ybasis: Point2D = .{0, 1}, - origin: Point2D = @splat(0), - color: color.Compressed = color.compress(color.white), - depth: f32 = 0, - texture_offset: Point2D = @splat(0), - texture_size: Point2D = @splat(1), -}; + pub const Texture = struct { + data: []const coral.io.Byte, + width: u16, + format: Format, + access: Access, -const Node = struct { - buffers: [2]Buffer, - is_swapped: bool = false, - ref_count: AtomicCount = AtomicCount.init(1), - has_next: ?*Node = null, - has_prev: ?*Node = null, + pub const Access = enum { + static, + }; - fn acquire(self: *Node) void { - self.ref_count.fetchAdd(1, .monotonic); - } + pub const Format = enum { + rgba8888, + bgra8888, + argb8888, + rgb888, + bgr888, - fn pending(self: *Node) *Buffer { - return &self.buffers[@intFromBool(self.is_swapped)]; - } - - fn release(self: *Node) 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; + pub fn byte_size(self: Format) usize { + return switch (self) { + .rgba8888, .bgra8888, .argb8888 => 4, + .rgb888, .bgr888 => 3, + }; } - - 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); - } - } - - fn submitted(self: *Node) *Buffer { - return &self.buffers[@intFromBool(!self.is_swapped)]; - } + }; + }; }; -pub const Point2D = @Vector(2, f32); +pub fn acquire(self: *Self) void { + self.ref_count.fetchAdd(1, .monotonic); +} -pub const Primitives = struct { - quad_mesh: Handle, -}; - -const Self = @This(); - -pub const State = struct { - node: *Node, - primitives: *const Primitives, -}; - -pub const Target = struct { - width: f32, - height: f32, -}; - -pub const Vertex2D = struct { - xy: Point2D, - uv: Point2D, -}; - -pub fn bind(context: coral.system.BindContext) std.mem.Allocator.Error!State { - const queue = try coral.heap.allocator.create(Node); +pub fn create() std.mem.Allocator.Error!*Self { + const queue = try coral.heap.allocator.create(Self); errdefer coral.heap.allocator.destroy(queue); @@ -266,68 +193,49 @@ pub fn bind(context: coral.system.BindContext) std.mem.Allocator.Error!State { has_tail = queue; - 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)).?; - }, - - .node = queue, - }; + return queue; } -pub fn consume_submitted(consumer: Consumer, target: Target) std.mem.Allocator.Error!void { - mutex.lock(); +pub fn pending(self: *Self) *Buffer { + return &self.buffers[@intFromBool(self.is_swapped)]; +} - defer mutex.unlock(); +pub fn release(self: *Self) void { + if (self.ref_count.fetchSub(1, .monotonic) == 1) { + mutex.lock(); - var has_node = has_head; - var iterations = @as(usize, 0); + defer mutex.unlock(); - while (has_node) |node| : ({ - has_node = node.has_next; - iterations += 1; - }) { - const buffer = &node.buffers[@intFromBool(!node.is_swapped)]; + if (self.has_prev) |prev| { + prev.has_next = self.has_next; + } else { + has_head = self.has_next; + } - try consumer.yield(.{buffer, target}); + if (self.has_next) |next| { + next.has_prev = self.has_prev; + } else { + has_tail = self.has_prev; + } - buffer.clear(); + for (&self.buffers) |*buffer| { + buffer.deinit(); + } + + coral.heap.allocator.destroy(self); } } -var has_head = @as(?*Node, null); - -var has_tail = @as(?*Node, null); - -pub fn init(state: *State) Self { - return .{ - .buffer = state.node.pending(), - .primitives = state.primitives, - }; +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); @@ -341,9 +249,61 @@ pub fn swap() void { while (has_node) |node| : (has_node = node.has_next) { node.is_swapped = !node.is_swapped; + + node.pending().clear(); } } -pub fn unbind(state: *State) void { - state.node.release(); +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); + } + } } diff --git a/src/ona/gfx/color.zig b/src/ona/gfx/color.zig deleted file mode 100644 index 5c73182..0000000 --- a/src/ona/gfx/color.zig +++ /dev/null @@ -1,15 +0,0 @@ -pub const Compressed = @Vector(4, u8); - -pub const Value = @Vector(4, f32); - -pub const black = Value{0, 0, 0, 1}; - -pub fn compress(color: Value) Compressed { - return @intFromFloat(color * @as(Value, @splat(255))); -} - -pub fn rgb(r: f32, g: f32, b: f32) Value { - return .{r, g, b, 1}; -} - -pub const white = Value{1, 1, 1, 1}; diff --git a/src/ona/gfx/shaders/draw_2d.glsl b/src/ona/gfx/shaders/draw_2d.glsl index 08c7164..2ffff17 100644 --- a/src/ona/gfx/shaders/draw_2d.glsl +++ b/src/ona/gfx/shaders/draw_2d.glsl @@ -8,7 +8,7 @@ in vec2 mesh_uv; in vec2 instance_xbasis; in vec2 instance_ybasis; in vec2 instance_origin; -in vec4 instance_color; +in vec4 instance_tint; in float instance_depth; in vec4 instance_rect; @@ -29,7 +29,7 @@ void main() { // Set the position of the vertex in clip space gl_Position = vec4(ndc_position, instance_depth, 1.0); - color = instance_color; + color = instance_tint; // Calculate the width and height from left, top, right, bottom configuration const vec2 rect_pos = instance_rect.xy; // left, top