diff --git a/src/main.zig b/src/main.zig index 93e26de..6a79895 100644 --- a/src/main.zig +++ b/src/main.zig @@ -6,8 +6,9 @@ const ona = @import("ona"); const Actors = struct { instances: coral.stack.Sequential(ona.gfx.Point2D) = .{.allocator = coral.heap.allocator}, - quad_mesh_2d: ona.gfx.Handle = .none, - body_texture: ona.gfx.Handle = .none, + quad_mesh_2d: ?*ona.gfx.Handle = null, + body_texture: ?*ona.gfx.Handle = null, + render_texture: ?*ona.gfx.Handle = null, }; const Player = struct { @@ -24,8 +25,21 @@ pub fn main() !void { fn load(display: coral.Write(ona.gfx.Display), actors: coral.Write(Actors), assets: coral.Write(ona.gfx.Assets)) !void { display.res.width, display.res.height = .{1280, 720}; - actors.res.body_texture = try assets.res.open_file(coral.files.bundle, "actor.bmp"); - actors.res.quad_mesh_2d = try assets.res.open_quad_mesh_2d(@splat(1)); + actors.res.body_texture = try assets.res.create_from_file(coral.files.bundle, "actor.bmp"); + actors.res.quad_mesh_2d = try assets.res.create_quad_mesh_2d(@splat(1)); + + actors.res.render_texture = try assets.res.context.create(.{ + .texture = .{ + .format = .rgba8, + + .access = .{ + .render = .{ + .width = display.res.width, + .height = display.res.height, + }, + }, + }, + }); try actors.res.instances.push_grow(.{0, 0}); } @@ -34,27 +48,47 @@ fn exit(actors: coral.Write(Actors)) void { actors.res.instances.deinit(); } -fn render(queue: ona.gfx.Queue, actors: coral.Write(Actors)) !void { - for (actors.res.instances.values) |instance| { - try queue.commands.append(.{ - .instance_2d = .{ - .mesh_2d = actors.res.quad_mesh_2d, - .texture = actors.res.body_texture, +fn render(queue: ona.gfx.Queue, actors: coral.Write(Actors), display: coral.Read(ona.gfx.Display)) !void { + try queue.commands.append(.{.target = .{ + .texture = actors.res.render_texture.?, + .clear_color = .{0, 0, 0, 0}, + .clear_depth = 0, + }}); - .transform = .{ - .origin = instance, - .xbasis = .{64, 0}, - .ybasis = .{0, 64}, - }, + for (actors.res.instances.values) |instance| { + try queue.commands.append(.{.instance_2d = .{ + .mesh_2d = actors.res.quad_mesh_2d.?, + .texture = actors.res.body_texture.?, + + .transform = .{ + .origin = instance, + .xbasis = .{64, 0}, + .ybasis = .{0, 64}, }, - }); + }}); } + + try queue.commands.append(.{.target = .{ + .clear_color = null, + .clear_depth = null, + }}); + + try queue.commands.append(.{.instance_2d = .{ + .mesh_2d = actors.res.quad_mesh_2d.?, + .texture = actors.res.render_texture.?, + + .transform = .{ + .origin = .{@floatFromInt(display.res.width / 2), @floatFromInt(display.res.height / 2)}, + .xbasis = .{@floatFromInt(display.res.width), 0}, + .ybasis = .{0, @floatFromInt(display.res.height)}, + }, + }}); } fn update(player: coral.Read(Player), actors: coral.Write(Actors), mapping: coral.Read(ona.act.Mapping)) !void { actors.res.instances.values[0] += .{ - mapping.res.axis_strength(player.res.move_x), - mapping.res.axis_strength(player.res.move_y), + mapping.res.axis_strength(player.res.move_x) * 10, + mapping.res.axis_strength(player.res.move_y) * 10, }; } diff --git a/src/ona/gfx.zig b/src/ona/gfx.zig index d998ee2..50c973b 100644 --- a/src/ona/gfx.zig +++ b/src/ona/gfx.zig @@ -26,7 +26,7 @@ pub const Assets = struct { }; }; - pub fn open_file(self: *Assets, storage: coral.files.Storage, path: []const u8) (OpenError || Format.Error)!Handle { + pub fn create_from_file(self: *Assets, storage: coral.files.Storage, path: []const u8) (OpenError || Format.Error)!*Handle { defer { const max_cache_size = 536870912; @@ -40,16 +40,16 @@ pub const Assets = struct { continue; } - return self.context.open(try format.file_desc(&self.staging_arena, storage, path)); + return self.context.create(try format.file_desc(&self.staging_arena, storage, path)); } - return .none; + return error.FormatUnsupported; } - pub fn open_quad_mesh_2d(self: *Assets, extents: Point2D) OpenError!Handle { + pub fn create_quad_mesh_2d(self: *Assets, extents: Point2D) OpenError!*Handle { const width, const height = extents / @as(Point2D, @splat(2)); - return self.context.open(.{ + return self.context.create(.{ .mesh_2d = .{ .indices = &.{0, 1, 2, 0, 2, 3}, @@ -66,6 +66,23 @@ pub const Assets = struct { pub const Color = @Vector(4, f32); +pub const Command = union (enum) { + instance_2d: Instance2D, + target: Target, + + pub const Instance2D = struct { + texture: *Handle, + mesh_2d: *Handle, + transform: Transform2D, + }; + + pub const Target = struct { + texture: ?*Handle = null, + clear_color: ?Color, + clear_depth: ?f32, + }; +}; + pub const Desc = union (enum) { texture: Texture, mesh_2d: Mesh2D, @@ -81,13 +98,19 @@ pub const Desc = union (enum) { }; pub const Texture = struct { - data: []const coral.io.Byte, - width: u16, format: Format, access: Access, - pub const Access = enum { - static, + pub const Access = union (enum) { + static: struct { + width: u16, + data: []const coral.io.Byte, + }, + + render: struct { + width: u16, + height: u16, + }, }; pub const Format = enum { @@ -109,17 +132,7 @@ pub const Display = struct { clear_color: Color = colors.black, }; -pub const Handle = enum (usize) { - none, - _, - - pub fn index(self: Handle) ?usize { - return switch (self) { - .none => null, - _ => @intFromEnum(self) - 1, - }; - } -}; +pub const Handle = opaque {}; pub const Input = union (enum) { key_up: Key, diff --git a/src/ona/gfx/device.zig b/src/ona/gfx/device.zig index eb2f203..679475e 100644 --- a/src/ona/gfx/device.zig +++ b/src/ona/gfx/device.zig @@ -2,6 +2,8 @@ const commands = @import("./commands.zig"); const coral = @import("coral"); +const default_2d = @import("./shaders/default_2d.glsl.zig"); + const ext = @import("../ext.zig"); const gfx = @import("../gfx.zig"); @@ -22,15 +24,30 @@ pub const Context = struct { renders: []RenderChain, }; - pub fn close(self: *Context, handle: gfx.Handle) void { - const handle_index = handle.index() orelse { - return; - }; - + pub fn close(self: *Context, handle: *gfx.Handle) void { const close_commands = self.loop.closes.pending(); std.debug.assert(close_commands.stack.cap > close_commands.stack.len()); - close_commands.append(.{.index = handle_index}) catch unreachable; + close_commands.append(handle) catch unreachable; + } + + pub fn create(self: *Context, desc: gfx.Desc) gfx.OpenError!*gfx.Handle { + const resource = try coral.heap.allocator.create(Resource); + + errdefer coral.heap.allocator.destroy(resource); + + try self.loop.creations.pending().append(.{ + .resource = resource, + .desc = desc, + }); + + const pending_destroys = self.loop.destroys.pending(); + + if (pending_destroys.stack.len() == pending_destroys.stack.cap) { + try pending_destroys.stack.grow(1); + } + + return @ptrCast(resource); } pub fn deinit(self: *Context) void { @@ -80,26 +97,6 @@ pub const Context = struct { }; } - pub fn open(self: *Context, desc: gfx.Desc) gfx.OpenError!gfx.Handle { - const open_commands = self.loop.opens.pending(); - const index = self.loop.closed_indices.get() orelse open_commands.stack.len(); - - try open_commands.append(.{ - .index = index, - .desc = desc, - }); - - const pending_closes = self.loop.closes.pending(); - - if (pending_closes.stack.len() == pending_closes.stack.cap) { - try pending_closes.stack.grow(1); - } - - _ = self.loop.closed_indices.pop(); - - return @enumFromInt(index + 1); - } - pub fn submit(self: *Context, submission: Submission) void { self.loop.finished.wait(); @@ -109,8 +106,8 @@ pub const Context = struct { render.swap(); } - self.loop.opens.swap(); - self.loop.closes.swap(); + self.loop.creations.swap(); + self.loop.destroys.swap(); var last_width, var last_height = [_]c_int{0, 0}; @@ -127,37 +124,57 @@ pub const Context = struct { } }; +const Frame = struct { + projection: default_2d.Mat4 = .{@splat(0), @splat(0), @splat(0), @splat(0)}, + flushed_batch_count: usize = 0, + pushed_batch_count: usize = 0, + render_passes: usize = 0, + mesh: ?*gfx.Handle = null, + texture: ?*gfx.Handle = null, + material: ?*gfx.Handle = null, +}; + const Loop = struct { ready: std.Thread.Semaphore = .{}, finished: std.Thread.Semaphore = .{}, clear_color: gfx.Color = gfx.colors.black, is_running: AtomicBool = AtomicBool.init(true), renders: []RenderChain = &.{}, - closes: CloseChain = CloseChain.init(coral.heap.allocator), - opens: OpenChain = OpenChain.init(coral.heap.allocator), - closed_indices: coral.stack.Sequential(usize) = .{.allocator = coral.heap.allocator}, + destroys: DestroyChain = DestroyChain.init(coral.heap.allocator), + creations: CreateChain = CreateChain.init(coral.heap.allocator), + has_resource_head: ?*Resource = null, + has_resource_tail: ?*Resource = null, const AtomicBool = std.atomic.Value(bool); - const CloseCommand = struct { - index: usize, - }; - - const OpenCommand = struct { - index: usize, + const CreateCommand = struct { + resource: *Resource, desc: gfx.Desc, - fn clone(command: OpenCommand, arena: *std.heap.ArenaAllocator) std.mem.Allocator.Error!OpenCommand { + fn clone(command: CreateCommand, arena: *std.heap.ArenaAllocator) std.mem.Allocator.Error!CreateCommand { const allocator = arena.allocator(); return .{ .desc = switch (command.desc) { .texture => |texture| .{ .texture = .{ - .data = try allocator.dupe(coral.io.Byte, texture.data), - .width = texture.width, + .access = switch (texture.access) { + .static => |static| .{ + .static = .{ + .data = try allocator.dupe(coral.io.Byte, static.data), + .width = static.width, + }, + }, + + .render => |render| .{ + .render = .{ + .width = render.width, + .height = render.height, + }, + }, + }, + .format = texture.format, - .access = texture.access, }, }, @@ -169,19 +186,162 @@ const Loop = struct { }, }, - .index = command.index, + .resource = command.resource, }; } }; - const CloseChain = commands.Chain(CloseCommand, null); + const CreateChain = commands.Chain(CreateCommand, CreateCommand.clone); - const OpenChain = commands.Chain(OpenCommand, OpenCommand.clone); + const DestroyChain = commands.Chain(*Resource, null); + + fn consume_creations(self: *Loop) std.mem.Allocator.Error!void { + const create_commands = self.creations.submitted(); + + defer create_commands.clear(); + + for (create_commands.stack.values) |command| { + errdefer coral.heap.allocator.destroy(command.resource); + + command.resource.* = .{ + .payload = switch (command.desc) { + .texture => |texture| init: { + const pixel_format = switch (texture.format) { + .rgba8 => sokol.gfx.PixelFormat.RGBA8, + .bgra8 => sokol.gfx.PixelFormat.BGRA8, + }; + + switch (texture.access) { + .static => |static| { + break: init .{ + .texture = .{ + .image = sokol.gfx.makeImage(.{ + .width = static.width, + .height = @intCast(static.data.len / (static.width * texture.format.byte_size())), + .pixel_format = pixel_format, + + .data = .{ + .subimage = get: { + var subimage = [_][16]sokol.gfx.Range{.{.{}} ** 16} ** 6; + + subimage[0][0] = sokol.gfx.asRange(static.data); + + break: get subimage; + }, + }, + }), + + .sampler = sokol.gfx.makeSampler(.{}), + }, + }; + }, + + .render => |render| { + const color_image = sokol.gfx.makeImage(.{ + .width = render.width, + .height = render.height, + .render_target = true, + .pixel_format = pixel_format, + }); + + const depth_image = sokol.gfx.makeImage(.{ + .width = render.width, + .height = render.height, + .render_target = true, + .pixel_format = .DEPTH_STENCIL, + }); + + break: init .{ + .render_target = .{ + .attachments = sokol.gfx.makeAttachments(.{ + .colors = get: { + var attachments = [_]sokol.gfx.AttachmentDesc{.{}} ** 4; + + attachments[0] = .{ + .image = color_image, + }; + + break: get attachments; + }, + + .depth_stencil = .{ + .image = depth_image, + }, + }), + + .sampler = sokol.gfx.makeSampler(.{}), + .color_image = color_image, + .depth_image = depth_image, + .width = render.width, + .height = render.height, + }, + }; + }, + } + }, + + .mesh_2d => |mesh_2d| init: { + if (mesh_2d.indices.len > std.math.maxInt(u32)) { + return error.OutOfMemory; + } + + break: init .{ + .mesh_2d = .{ + .index_buffer = sokol.gfx.makeBuffer(.{ + .data = sokol.gfx.asRange(mesh_2d.indices), + .type = .INDEXBUFFER, + }), + + .vertex_buffer = sokol.gfx.makeBuffer(.{ + .data = sokol.gfx.asRange(mesh_2d.vertices), + .type = .VERTEXBUFFER, + }), + + .index_count = @intCast(mesh_2d.indices.len), + }, + }; + }, + }, + + .has_prev = self.has_resource_tail, + .has_next = null, + }; + + if (self.has_resource_tail) |resource_tail| { + resource_tail.has_next = command.resource; + } else { + std.debug.assert(self.has_resource_head == null); + + self.has_resource_head = command.resource; + } + + self.has_resource_tail = command.resource; + } + } + + fn consume_destroys(self: *Loop) std.mem.Allocator.Error!void { + const destroy_commands = self.destroys.submitted(); + + defer destroy_commands.clear(); + + for (destroy_commands.stack.values) |resource| { + defer coral.heap.allocator.destroy(resource); + + resource.destroy(); + + if (resource.has_prev) |resource_prev| { + resource_prev.has_next = resource.has_next; + } + + if (resource.has_next) |resource_next| { + resource_next.has_prev = resource.has_prev; + } + } + } fn deinit(self: *Loop) void { - self.closes.deinit(); - self.opens.deinit(); - self.closed_indices.deinit(); + self.destroys.deinit(); + self.creations.deinit(); } fn run(self: *Loop, window: *ext.SDL_Window) !void { @@ -220,9 +380,9 @@ const Loop = struct { ext.SDL_GL_DeleteContext(context); } - var rendering = Rendering.init(); + var rendering_2d = Rendering2D.init(); - defer rendering.deinit(); + defer rendering_2d.deinit(); self.finished.post(); @@ -231,230 +391,219 @@ const Loop = struct { defer self.finished.post(); - const open_commands = self.opens.submitted(); + try self.consume_creations(); - defer open_commands.clear(); + var frame = Frame{}; - for (open_commands.stack.values) |command| { - switch (command.desc) { - .texture => |texture| { - const stride = texture.width * texture.format.byte_size(); - - const image = sokol.gfx.makeImage(.{ - .width = texture.width, - .height = @intCast(texture.data.len / stride), - - .data = .{ - .subimage = get: { - var subimage = [_][16]sokol.gfx.Range{.{.{}} ** 16} ** 6; - - subimage[0][0] = sokol.gfx.asRange(texture.data); - - break: get subimage; - }, - }, - - .pixel_format = switch (texture.format) { - .rgba8 => .RGBA8, - .bgra8 => .BGRA8, - }, - }); - - errdefer sokol.gfx.destroyImage(image); - - const sampler = sokol.gfx.makeSampler(.{}); - - errdefer sokol.gfx.destroySampler(sampler); - - try rendering.insert_object(command.index, .{ - .texture = .{ - .sampler = sampler, - .image = image, - }, - }); - }, - - .mesh_2d => |mesh_2d| { - const index_buffer = sokol.gfx.makeBuffer(.{ - .data = sokol.gfx.asRange(mesh_2d.indices), - .type = .INDEXBUFFER, - }); - - const vertex_buffer = sokol.gfx.makeBuffer(.{ - .data = sokol.gfx.asRange(mesh_2d.vertices), - .type = .VERTEXBUFFER, - }); - - errdefer { - sokol.gfx.destroyBuffer(index_buffer); - sokol.gfx.destroyBuffer(vertex_buffer); - } - - if (mesh_2d.indices.len > std.math.maxInt(u32)) { - return error.OutOfMemory; - } - - try rendering.insert_object(command.index, .{ - .mesh_2d = .{ - .index_buffer = index_buffer, - .vertex_buffer = vertex_buffer, - .index_count = @intCast(mesh_2d.indices.len), - }, - }); - }, - } - } - - var frame = init_frame: { - var width, var height = [_]c_int{0, 0}; - - ext.SDL_GL_GetDrawableSize(window, &width, &height); - std.debug.assert(width > 0 and height > 0); - - break: init_frame Rendering.Frame{ - .width = @intCast(width), - .height = @intCast(height), - }; - }; - - sokol.gfx.beginPass(.{ - .swapchain = .{ - .width = frame.width, - .height = frame.height, - .sample_count = 1, - .color_format = .RGBA8, - .depth_format = .DEPTH_STENCIL, - .gl = .{.framebuffer = 0}, - }, - - .action = .{ - .colors = get: { - var actions = [_]sokol.gfx.ColorAttachmentAction{.{}} ** 4; - - actions[0] = .{ - .load_action = .CLEAR, - .clear_value = @as(sokol.gfx.Color, @bitCast(self.clear_color)), - }; - - break: get actions; - }, - }, + rendering_2d.set_target(&frame, window, .{ + .clear_color = self.clear_color, + .clear_depth = 0, }); - for (self.renders) |*render| { - const render_commands = render.submitted(); + if (self.renders.len != 0) { + const renders_tail_index = self.renders.len - 1; + + for (self.renders[0 .. renders_tail_index]) |*render| { + const render_commands = render.submitted(); + + defer render_commands.clear(); + + for (render_commands.stack.values) |command| { + try switch (command) { + .instance_2d => |instance_2d| rendering_2d.batch(&frame, instance_2d), + .target => |target| rendering_2d.set_target(&frame, window, target), + }; + } + + rendering_2d.set_target(&frame, window, .{ + .clear_color = null, + .clear_depth = null, + }); + } + + const render_commands = self.renders[renders_tail_index].submitted(); defer render_commands.clear(); for (render_commands.stack.values) |command| { - 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; - }, - } + try switch (command) { + .instance_2d => |instance_2d| rendering_2d.batch(&frame, instance_2d), + .target => |target| rendering_2d.set_target(&frame, window, target), + }; } + + rendering_2d.flush(&frame); } - rendering.flush_instance_2ds(&frame); sokol.gfx.endPass(); sokol.gfx.commit(); ext.SDL_GL_SwapWindow(window); - const close_commands = self.closes.submitted(); + try self.consume_destroys(); + } - defer close_commands.clear(); + var has_next_resource = self.has_resource_head; - for (close_commands.stack.values) |command| { - const object = &rendering.objects.values[command.index]; + while (has_next_resource) |resource| { + std.log.info("destroying remaining gfx device resource {x}", .{@intFromPtr(resource)}); + resource.destroy(); - switch (object.*) { - .empty => {}, // TODO: Handle double-closes. + has_next_resource = resource.has_next; - .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; - } + coral.heap.allocator.destroy(resource); } } }; -const Rendering = struct { - objects: coral.stack.Sequential(Object), - instance_2d_pipeline: sokol.gfx.Pipeline, - instance_2d_buffers: coral.stack.Sequential(sokol.gfx.Buffer), +pub const RenderChain = commands.Chain(gfx.Command, clone_command); - const Instance2D = extern struct { +pub const RenderList = commands.List(gfx.Command, clone_command); + +const Rendering2D = struct { + batching_pipeline: sokol.gfx.Pipeline, + batching_buffers: coral.stack.Sequential(sokol.gfx.Buffer), + + const Instance = extern struct { transform: gfx.Transform2D, tint: @Vector(4, u8) = @splat(std.math.maxInt(u8)), depth: f32 = 0, texture_offset: gfx.Point2D = @splat(0), texture_size: gfx.Point2D = @splat(1), - - const buffer_indices = .{ - .mesh = 0, - .instance = 1, - }; - - 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, + const buffer_indices = .{ + .mesh = 0, + .instance = 1, + }; - fn unflushed_instance_2d_count(self: Frame) usize { - return self.pushed_instance_2d_count - self.flushed_instance_2d_count; + fn batch(self: *Rendering2D, frame: *Frame, instance: gfx.Command.Instance2D) std.mem.Allocator.Error!void { + if (instance.mesh_2d != frame.mesh or instance.texture != frame.texture) { + self.flush(frame); } - }; - const Object = union (enum) { - empty, + frame.mesh = instance.mesh_2d; + frame.texture = instance.texture; - mesh_2d: struct { - index_count: u32, - vertex_buffer: sokol.gfx.Buffer, - index_buffer: sokol.gfx.Buffer, - }, + const has_filled_buffer = (frame.pushed_batch_count % instances_per_buffer) == 0; + const pushed_buffer_count = frame.pushed_batch_count / instances_per_buffer; - texture: struct { - image: sokol.gfx.Image, - sampler: sokol.gfx.Sampler, - }, - }; + if (has_filled_buffer and pushed_buffer_count == self.batching_buffers.len()) { + var name_buffer = [_:0]u8{0} ** 64; - fn deinit(self: *Rendering) void { - for (self.instance_2d_buffers.values) |buffer| { + const name_view = std.fmt.bufPrint(&name_buffer, "instance 2d buffer #{}", .{self.batching_buffers.len()}) catch { + unreachable; + }; + + const instance_buffer = sokol.gfx.makeBuffer(.{ + .label = name_view.ptr, + .size = @sizeOf(Instance) * instances_per_buffer, + .usage = .STREAM, + }); + + errdefer sokol.gfx.destroyBuffer(instance_buffer); + + try self.batching_buffers.push_grow(instance_buffer); + } + + _ = sokol.gfx.appendBuffer(self.batching_buffers.get().?, sokol.gfx.asRange(&Instance{ + .transform = instance.transform, + })); + + frame.pushed_batch_count += 1; + } + + fn deinit(self: *Rendering2D) void { + for (self.batching_buffers.values) |buffer| { sokol.gfx.destroyBuffer(buffer); } - self.instance_2d_buffers.deinit(); - sokol.gfx.destroyPipeline(self.instance_2d_pipeline); - self.objects.deinit(); + self.batching_buffers.deinit(); + sokol.gfx.destroyPipeline(self.batching_pipeline); } - fn init() Rendering { + fn flush(self: *Rendering2D, frame: *Frame) void { + const unflushed_count = frame.pushed_batch_count - frame.flushed_batch_count; + + if (unflushed_count == 0) { + return; + } + + sokol.gfx.applyPipeline(self.batching_pipeline); + + sokol.gfx.applyUniforms(.VS, default_2d.SLOT_Projection, sokol.gfx.asRange(&default_2d.Projection{ + .projection = frame.projection, + })); + + const mesh_2d = resource_cast(frame.mesh.?).payload.mesh_2d; + + var bindings = sokol.gfx.Bindings{ + .vertex_buffers = get: { + var buffers = [_]sokol.gfx.Buffer{.{}} ** 8; + + buffers[buffer_indices.mesh] = mesh_2d.vertex_buffer; + + break: get buffers; + }, + + .index_buffer = mesh_2d.index_buffer, + + .fs = switch (resource_cast(frame.texture.?).payload) { + .texture => |texture| .{ + .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; + }, + }, + + .render_target => |render_target| .{ + .images = get: { + var images = [_]sokol.gfx.Image{.{}} ** 12; + + images[0] = render_target.color_image; + + break: get images; + }, + + .samplers = get: { + var samplers = [_]sokol.gfx.Sampler{.{}} ** 8; + + samplers[0] = render_target.sampler; + + break: get samplers; + }, + }, + + else => unreachable, + }, + }; + + while (frame.flushed_batch_count < frame.pushed_batch_count) { + const buffer_index = frame.flushed_batch_count / instances_per_buffer; + const buffer_offset = frame.flushed_batch_count % instances_per_buffer; + const instances_to_flush = @min(instances_per_buffer - buffer_offset, unflushed_count); + + bindings.vertex_buffers[buffer_indices.instance] = self.batching_buffers.values[buffer_index]; + bindings.vertex_buffer_offsets[buffer_indices.instance] = @intCast(@sizeOf(Instance) * buffer_offset); + + sokol.gfx.applyBindings(bindings); + sokol.gfx.draw(0, mesh_2d.index_count, @intCast(instances_to_flush)); + + frame.flushed_batch_count += instances_to_flush; + } + } + + fn init() Rendering2D { sokol.gfx.setup(.{ .environment = .{ .defaults = .{ @@ -470,51 +619,51 @@ const Rendering = struct { }); return .{ - .instance_2d_pipeline = sokol.gfx.makePipeline(.{ + .batching_pipeline = sokol.gfx.makePipeline(.{ .label = "2D drawing pipeline", .layout = .{ .attrs = get: { var attrs = [_]sokol.gfx.VertexAttrState{.{}} ** 16; - attrs[Instance2D.shader.ATTR_vs_mesh_xy] = .{ + attrs[default_2d.ATTR_vs_mesh_xy] = .{ .format = .FLOAT2, - .buffer_index = Instance2D.buffer_indices.mesh, + .buffer_index = buffer_indices.mesh, }; - attrs[Instance2D.shader.ATTR_vs_mesh_uv] = .{ + attrs[default_2d.ATTR_vs_mesh_uv] = .{ .format = .FLOAT2, - .buffer_index = Instance2D.buffer_indices.mesh, + .buffer_index = buffer_indices.mesh, }; - attrs[Instance2D.shader.ATTR_vs_instance_xbasis] = .{ + attrs[default_2d.ATTR_vs_instance_xbasis] = .{ .format = .FLOAT2, - .buffer_index = Instance2D.buffer_indices.instance, + .buffer_index = buffer_indices.instance, }; - attrs[Instance2D.shader.ATTR_vs_instance_ybasis] = .{ + attrs[default_2d.ATTR_vs_instance_ybasis] = .{ .format = .FLOAT2, - .buffer_index = Instance2D.buffer_indices.instance, + .buffer_index = buffer_indices.instance, }; - attrs[Instance2D.shader.ATTR_vs_instance_origin] = .{ + attrs[default_2d.ATTR_vs_instance_origin] = .{ .format = .FLOAT2, - .buffer_index = Instance2D.buffer_indices.instance, + .buffer_index = buffer_indices.instance, }; - attrs[Instance2D.shader.ATTR_vs_instance_tint] = .{ + attrs[default_2d.ATTR_vs_instance_tint] = .{ .format = .UBYTE4N, - .buffer_index = Instance2D.buffer_indices.instance, + .buffer_index = buffer_indices.instance, }; - attrs[Instance2D.shader.ATTR_vs_instance_depth] = .{ + attrs[default_2d.ATTR_vs_instance_depth] = .{ .format = .FLOAT, - .buffer_index = Instance2D.buffer_indices.instance, + .buffer_index = buffer_indices.instance, }; - attrs[Instance2D.shader.ATTR_vs_instance_rect] = .{ + attrs[default_2d.ATTR_vs_instance_rect] = .{ .format = .FLOAT4, - .buffer_index = Instance2D.buffer_indices.instance, + .buffer_index = buffer_indices.instance, }; break: get attrs; @@ -523,169 +672,152 @@ const Rendering = struct { .buffers = get: { var buffers = [_]sokol.gfx.VertexBufferLayoutState{.{}} ** 8; - buffers[Instance2D.buffer_indices.instance].step_func = .PER_INSTANCE; + buffers[buffer_indices.instance].step_func = .PER_INSTANCE; break: get buffers; }, }, - .shader = sokol.gfx.makeShader(Instance2D.shader.draw2dShaderDesc(sokol.gfx.queryBackend())), + .shader = sokol.gfx.makeShader(default_2d.draw2dShaderDesc(sokol.gfx.queryBackend())), .index_type = .UINT16, }), - .instance_2d_buffers = .{.allocator = coral.heap.allocator}, - .objects = .{.allocator = coral.heap.allocator}, + .batching_buffers = .{.allocator = coral.heap.allocator}, }; } - fn flush_instance_2ds(self: *Rendering, frame: *Frame) void { - const unflushed_count = frame.unflushed_instance_2d_count(); + const instances_per_buffer = 512; - if (unflushed_count == 0) { - return; + fn set_target(self: *Rendering2D, frame: *Frame, window: *ext.SDL_Window, target: gfx.Command.Target) void { + defer frame.render_passes += 1; + + if (frame.render_passes != 0) { + self.flush(frame); + sokol.gfx.endPass(); } - 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; - }, + var pass = sokol.gfx.Pass{ + .action = .{ + // TODO: Review if stencil buffer is needed. + .stencil = .{.load_action = .CLEAR}, }, }; - 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; + if (target.clear_color) |color| { + pass.action.colors[0] = .{ + .load_action = .CLEAR, + .clear_value = @bitCast(color), + }; } 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); + pass.action.colors[0] = .{.load_action = .LOAD}; } - 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); + if (target.clear_depth) |depth| { + pass.action.depth = .{ + .load_action = .CLEAR, + .clear_value = depth, + }; + } else { + pass.action.depth = .{.load_action = .LOAD}; } - _ = sokol.gfx.appendBuffer(self.instance_2d_buffers.get().?, sokol.gfx.asRange(&Instance2D{ - .transform = command.transform, - })); + if (target.texture) |render_texture| { + const render_target = resource_cast(render_texture).payload.render_target; - frame.pushed_instance_2d_count += 1; - } + pass.attachments = render_target.attachments; + frame.projection = ortho_projection(0.0, @floatFromInt(render_target.width), 0.0, @floatFromInt(render_target.height), -1.0, 1.0); + } else { + var target_width, var target_height = [_]c_int{0, 0}; - fn remove_object(self: *Rendering, index: usize) ?Object { - const object = self.objects.values[index]; + ext.SDL_GL_GetDrawableSize(window, &target_width, &target_height); + std.debug.assert(target_width > 0 and target_height > 0); - if (object != .empty) { - self.objects.values[index] = .empty; + pass.swapchain = .{ + .width = target_width, + .height = target_height, + .sample_count = 1, + .color_format = .RGBA8, + .depth_format = .DEPTH_STENCIL, + .gl = .{.framebuffer = 0}, + }; - return object; + frame.projection = ortho_projection(0.0, @floatFromInt(target_width), @floatFromInt(target_height), 0.0, -1.0, 1.0); } - return null; + sokol.gfx.beginPass(pass); } }; -pub const RenderCommand = union (enum) { - instance_2d: Instance, - post_process: PostProcess, +const Resource = struct { + has_prev: ?*Resource, + has_next: ?*Resource, - pub const Instance = struct { - texture: gfx.Handle, - mesh_2d: gfx.Handle, - transform: gfx.Transform2D, - }; + payload: union (enum) { + mesh_2d: struct { + index_count: u32, + vertex_buffer: sokol.gfx.Buffer, + index_buffer: sokol.gfx.Buffer, + }, - pub const PostProcess = struct { + texture: struct { + image: sokol.gfx.Image, + sampler: sokol.gfx.Sampler, + }, - }; + render_target: struct { + sampler: sokol.gfx.Sampler, + color_image: sokol.gfx.Image, + depth_image: sokol.gfx.Image, + attachments: sokol.gfx.Attachments, + width: u16, + height: u16, + }, + }, - fn clone(self: RenderCommand, arena: *std.heap.ArenaAllocator) std.mem.Allocator.Error!RenderCommand { - _ = arena; + fn destroy(self: Resource) void { + switch (self.payload) { + .mesh_2d => |mesh_2d| { + sokol.gfx.destroyBuffer(mesh_2d.vertex_buffer); + sokol.gfx.destroyBuffer(mesh_2d.index_buffer); + }, - return switch (self) { - .instance_2d => |instance_2d| .{.instance_2d = instance_2d}, - .post_process => |post_process| .{.post_process = post_process}, - }; + .texture => |texture| { + sokol.gfx.destroyImage(texture.image); + sokol.gfx.destroySampler(texture.sampler); + }, + + .render_target => |render_target| { + sokol.gfx.destroyImage(render_target.color_image); + sokol.gfx.destroyImage(render_target.depth_image); + sokol.gfx.destroySampler(render_target.sampler); + sokol.gfx.destroyAttachments(render_target.attachments); + }, + } } }; -pub const RenderChain = commands.Chain(RenderCommand, RenderCommand.clone); +fn clone_command(self: gfx.Command, arena: *std.heap.ArenaAllocator) std.mem.Allocator.Error!gfx.Command { + _ = arena; -pub const RenderList = commands.List(RenderCommand, RenderCommand.clone); + return switch (self) { + .instance_2d => |instance_2d| .{.instance_2d = instance_2d}, + .target => |target| .{.target = target}, + }; +} + +fn resource_cast(handle: *gfx.Handle) *Resource { + return @ptrCast(@alignCast(handle)); +} + +fn ortho_projection(left: f32, right: f32, bottom: f32, top: f32, zNear: f32, zFar: f32) default_2d.Mat4 { + var result = default_2d.Mat4{.{1, 0, 0, 0}, .{0, 1, 0, 0}, .{0, 0, 1, 0}, .{0, 0, 0, 1}}; + + result[0][0] = 2 / (right - left); + result[1][1] = 2 / (top - bottom); + result[2][2] = - 2 / (zFar - zNear); + result[3][0] = - (right + left) / (right - left); + result[3][1] = - (top + bottom) / (top - bottom); + result[3][2] = - (zFar + zNear) / (zFar - zNear); + + return result; +} diff --git a/src/ona/gfx/formats.zig b/src/ona/gfx/formats.zig index a717cd4..a767d7f 100644 --- a/src/ona/gfx/formats.zig +++ b/src/ona/gfx/formats.zig @@ -72,10 +72,14 @@ pub fn bmp_file_desc( return .{ .texture = .{ - .width = pixel_width, - .data = pixels, .format = .rgba8, - .access = .static, + + .access = .{ + .static = .{ + .width = pixel_width, + .data = pixels, + }, + }, } }; } diff --git a/src/ona/gfx/shaders/instance_2d.glsl b/src/ona/gfx/shaders/default_2d.glsl similarity index 72% rename from src/ona/gfx/shaders/instance_2d.glsl rename to src/ona/gfx/shaders/default_2d.glsl index 160c15c..111abec 100644 --- a/src/ona/gfx/shaders/instance_2d.glsl +++ b/src/ona/gfx/shaders/default_2d.glsl @@ -1,5 +1,7 @@ -@header const Vec2 = @Vector(2, f32) +@header pub const Vec2 = @Vector(2, f32) +@header pub const Mat4 = [4]@Vector(4, f32) @ctype vec2 Vec2 +@ctype mat4 Mat4 @vs vs in vec2 mesh_xy; @@ -12,8 +14,8 @@ in vec4 instance_tint; in float instance_depth; in vec4 instance_rect; -uniform Screen { - vec2 screen_size; +uniform Projection { + mat4 projection; }; out vec4 color; @@ -22,13 +24,10 @@ out vec2 uv; void main() { // Calculate the world position of the vertex const vec2 world_position = instance_origin + mesh_xy.x * instance_xbasis + mesh_xy.y * instance_ybasis; - - // Convert world position to normalized device coordinates (NDC) - // Assuming the screen coordinates range from (0, 0) to (screen_size.x, screen_size.y) - const vec2 ndc_position = (vec2(world_position.x, -world_position.y) / screen_size) * 2.0 - vec2(1.0, -1.0); + const vec2 projected_position = (projection * vec4(world_position, 0, 1)).xy; // Set the position of the vertex in clip space - gl_Position = vec4(ndc_position, instance_depth, 1.0); + gl_Position = vec4(projected_position, instance_depth, 1.0); color = instance_tint; // Calculate the width and height from left, top, right, bottom configuration