From acaf58cf3c9e1c1378674bbbc73f8edb74dc3e3b Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Thu, 22 Jan 2026 01:15:19 +0200 Subject: [PATCH] separate frame specific data --- src/engine/frame.zig | 147 ++++++++++++++++++++++++- src/engine/graphics.zig | 204 +++++++++++++++++++---------------- src/engine/input.zig | 5 - src/engine/root.zig | 79 ++++++++++---- src/engine/screen_scaler.zig | 2 +- src/game.zig | 27 +++-- 6 files changed, 329 insertions(+), 135 deletions(-) diff --git a/src/engine/frame.zig b/src/engine/frame.zig index 16fea2d..22f5a28 100644 --- a/src/engine/frame.zig +++ b/src/engine/frame.zig @@ -1,4 +1,6 @@ const std = @import("std"); +const build_options = @import("build_options"); +const log = std.log.scoped(.engine); const InputSystem = @import("./input.zig"); const KeyCode = InputSystem.KeyCode; @@ -8,6 +10,16 @@ const AudioSystem = @import("./audio/root.zig"); const AudioData = AudioSystem.Data; const AudioCommand = AudioSystem.Command; +const GraphicsSystem = @import("./graphics.zig"); +const GraphicsCommand = GraphicsSystem.Command; +const Font = GraphicsSystem.Font; + +const Math = @import("./math.zig"); +const Rect = Math.Rect; +const Vec4 = Math.Vec4; +const Vec2 = Math.Vec2; +const rgb = Math.rgb; + pub const Nanoseconds = u64; const Frame = @This(); @@ -71,20 +83,42 @@ pub const Audio = struct { }; }; +pub const Graphics = struct { + clear_color: Vec4, + screen_size: Vec2, + canvas_size: ?Vec2, + + scissor_stack: std.ArrayList(Rect), + commands: std.ArrayList(GraphicsCommand), + + pub const empty = Graphics{ + .clear_color = rgb(0, 0, 0), + .screen_size = .init(0, 0), + .canvas_size = null, + .scissor_stack = .empty, + .commands = .empty + }; +}; + arena: std.heap.ArenaAllocator, time_ns: Nanoseconds, dt_ns: Nanoseconds, input: Input, audio: Audio, +graphics: Graphics, -pub fn init(gpa: std.mem.Allocator) Frame { - return Frame{ +show_debug: bool, + +pub fn init(self: *Frame, gpa: std.mem.Allocator) void { + self.* = Frame{ .arena = std.heap.ArenaAllocator.init(gpa), .time_ns = 0, .dt_ns = 0, .input = .empty, - .audio = .empty + .audio = .empty, + .graphics = .empty, + .show_debug = false }; } @@ -132,10 +166,115 @@ pub const PlayAudioOptions = struct { id: AudioData.Id, }; +fn pushAudioCommand(self: *Frame, command: AudioCommand) void { + const arena = self.arena.allocator(); + + self.audio.commands.append(arena, command) catch |e| { + log.warn("Failed to play audio: {}", .{e}); + }; +} + +fn pushGraphicsCommand(self: *Frame, command: GraphicsCommand) void { + const arena = self.arena.allocator(); + + self.graphics.commands.append(arena, command) catch |e|{ + log.warn("Failed to push graphics command: {}", .{e}); + }; +} + pub fn playAudio(self: *Frame, options: PlayAudioOptions) !void { - try self.audio.commands.append(self.arena.allocator(), .{ + self.pushAudioCommand(.{ .play = .{ .data_id = options.id } }); } + +pub fn pushScissor(self: *Frame, rect: Rect) void { + const arena = self.arena.allocator(); + self.graphics.scissor_stack.append(arena, rect) catch |e| { + log.warn("Failed to push scissor region: {}", .{e}); + return; + }; + + self.pushGraphicsCommand(.{ + .set_scissor = rect + }); +} + +pub fn popScissor(self: *Frame) void { + _ = self.graphics.scissor_stack.pop().?; + const rect = self.graphics.scissor_stack.getLast(); + + self.pushGraphicsCommand(.{ + .set_scissor = rect + }); +} + +pub fn drawRectangle(self: *Frame, pos: Vec2, size: Vec2, color: Vec4) void { + self.pushGraphicsCommand(.{ + .draw_rectangle = .{ + .pos = pos, + .size = size, + .color = color + } + }); +} + +pub fn drawLine(self: *Frame, pos1: Vec2, pos2: Vec2, color: Vec4, width: f32) void { + self.pushGraphicsCommand(.{ + .draw_line = .{ + .pos1 = pos1, + .pos2 = pos2, + .width = width, + .color = color + } + }); +} + +pub fn drawRectanglOutline(self: *Frame, pos: Vec2, size: Vec2, color: Vec4, width: f32) void { + // TODO: Don't use line segments + self.drawLine(pos, pos.add(.{ .x = size.x, .y = 0 }), color, width); + self.drawLine(pos, pos.add(.{ .x = 0, .y = size.y }), color, width); + self.drawLine(pos.add(.{ .x = 0, .y = size.y }), pos.add(size), color, width); + self.drawLine(pos.add(.{ .x = size.x, .y = 0 }), pos.add(size), color, width); +} + +pub const DrawTextOptions = struct { + font: Font.Id, + size: f32 = 16, + color: Vec4 = rgb(255, 255, 255), +}; + +pub fn drawText(self: *Frame, position: Vec2, text: []const u8, opts: DrawTextOptions) void { + const arena = self.arena.allocator(); + const text_dupe = arena.dupe(u8, text) catch |e| { + log.warn("Failed to draw text: {}", .{e}); + return; + }; + + self.pushGraphicsCommand(.{ + .draw_text = .{ + .pos = position, + .text = text_dupe, + .size = opts.size, + .font = opts.font, + .color = opts.color, + } + }); +} + +pub fn pushTransform(self: *Frame, translation: Vec2, scale: Vec2) void { + self.pushGraphicsCommand(.{ + .push_transformation = .{ + .translation = translation, + .scale = scale + } + }); +} + +pub fn popTransform(self: *Frame) void { + self.pushGraphicsCommand(.{ + .pop_transformation = {} + }); +} diff --git a/src/engine/graphics.zig b/src/engine/graphics.zig index d996475..4b56b13 100644 --- a/src/engine/graphics.zig +++ b/src/engine/graphics.zig @@ -21,6 +21,8 @@ const tracy = @import("tracy"); const fontstash = @import("./fontstash/root.zig"); pub const Font = fontstash.Font; +const GraphicsFrame = @import("./frame.zig").Graphics; + // TODO: Seems that there is a vertical jitter bug when resizing a window in OpenGL. Seems like a driver bug. // From other peoples research it seems that disabling vsync when a resize event occurs fixes it. // Maybe a patch for sokol could be made? @@ -39,30 +41,42 @@ const Options = struct { imgui_font: ?ImguiFont = null }; -const DrawFrame = struct { - screen_size: Vec2 = Vec2.zero, - bg_color: Vec4 = rgb(0, 0, 0), - - scissor_stack_buffer: [32]Rect = undefined, - scissor_stack: std.ArrayListUnmanaged(Rect) = .empty, - - fn init(self: *DrawFrame) void { - self.* = DrawFrame{ - .scissor_stack = .initBuffer(&self.scissor_stack_buffer) - }; - } +pub const Command = union(enum) { + set_scissor: Rect, + draw_rectangle: struct { + pos: Vec2, + size: Vec2, + color: Vec4 + }, + draw_line: struct { + pos1: Vec2, + pos2: Vec2, + color: Vec4, + width: f32 + }, + draw_text: struct { + pos: Vec2, + text: []const u8, + font: Font.Id, + size: f32, + color: Vec4, + }, + push_transformation: struct { + translation: Vec2, + scale: Vec2 + }, + pop_transformation: void }; -var draw_frame: DrawFrame = undefined; var main_pipeline: sgl.Pipeline = .{}; var linear_sampler: sg.Sampler = .{}; var nearest_sampler: sg.Sampler = .{}; var font_context: fontstash.Context = undefined; -pub var font_resolution_scale: f32 = 1; + +var scale_stack_buffer: [32]Vec2 = undefined; +var scale_stack: std.ArrayList(Vec2) = .empty; pub fn init(options: Options) !void { - draw_frame.init(); - sg.setup(.{ .logger = options.logger, .environment = sglue.environment(), @@ -140,43 +154,103 @@ pub fn deinit() void { sg.shutdown(); } +pub fn drawCommand(command: Command) void { + switch(command) { + .push_transformation => |opts| { + pushTransform(opts.translation, opts.scale); + // font_resolution_scale = font_resolution_scale.multiply(opts.scale); + }, + .pop_transformation => { + popTransform(); + }, + .draw_rectangle => |opts| { + drawRectangle(opts.pos, opts.size, opts.color); + }, + .set_scissor => |opts| { + sgl.scissorRectf( + opts.pos.x, + opts.pos.y, + opts.size.x, + opts.size.y, + true + ); + }, + .draw_line => |opts| { + drawLine( + opts.pos1, + opts.pos2, + opts.color, + opts.width + ); + }, + .draw_text => |opts| { + const font_resolution_scale = scale_stack.getLast(); + + sgl.pushMatrix(); + defer sgl.popMatrix(); + + sgl.scale(1/font_resolution_scale.x, 1/font_resolution_scale.y, 1); + + font_context.setFont(opts.font); + font_context.setSize(opts.size * font_resolution_scale.y); + font_context.setAlign(.{ .x = .left, .y = .top }); + font_context.setSpacing(0); + + const r: u8 = @intFromFloat(opts.color.x * 255); + const g: u8 = @intFromFloat(opts.color.y * 255); + const b: u8 = @intFromFloat(opts.color.z * 255); + const a: u8 = @intFromFloat(opts.color.w * 255); + const color: u32 = r | (@as(u32, g) << 8) | (@as(u32, b) << 16) | (@as(u32, a) << 24); + font_context.setColor(color); + font_context.drawText( + opts.pos.x * font_resolution_scale.x, + opts.pos.y * font_resolution_scale.y, + opts.text + ); + } + } +} + +pub fn drawCommands(commands: []const Command) void { + for (commands) |command| { + drawCommand(command); + } +} + pub fn beginFrame() void { const zone = tracy.initZone(@src(), .{ }); defer zone.deinit(); - draw_frame.init(); - draw_frame.screen_size = Vec2.init(sapp.widthf(), sapp.heightf()); - draw_frame.scissor_stack.appendAssumeCapacity(Rect.init(0, 0, sapp.widthf(), sapp.heightf())); - imgui.newFrame(.{ - .width = @intFromFloat(draw_frame.screen_size.x), - .height = @intFromFloat(draw_frame.screen_size.y), + .width = sapp.width(), + .height = sapp.height(), .delta_time = sapp.frameDuration(), .dpi_scale = sapp.dpiScale() }); + scale_stack = .initBuffer(&scale_stack_buffer); + scale_stack.appendAssumeCapacity(.init(1, 1)); + font_context.clearState(); sgl.defaults(); sgl.matrixModeProjection(); - sgl.ortho(0, draw_frame.screen_size.x, draw_frame.screen_size.y, 0, -1, 1); + sgl.ortho(0, sapp.widthf(), sapp.heightf(), 0, -1, 1); sgl.loadPipeline(main_pipeline); } -pub fn endFrame() void { +pub fn endFrame(clear_color: Vec4) void { const zone = tracy.initZone(@src(), .{ }); defer zone.deinit(); - assert(draw_frame.scissor_stack.items.len == 1); - var pass_action: sg.PassAction = .{}; pass_action.colors[0] = sg.ColorAttachmentAction{ .load_action = .CLEAR, .clear_value = .{ - .r = draw_frame.bg_color.x, - .g = draw_frame.bg_color.y, - .b = draw_frame.bg_color.z, - .a = draw_frame.bg_color.w + .r = clear_color.x, + .g = clear_color.y, + .b = clear_color.z, + .a = clear_color.w } }; @@ -196,17 +270,20 @@ pub fn endFrame() void { sg.commit(); } -pub fn pushTransform(translation: Vec2, scale: f32) void { +pub fn pushTransform(translation: Vec2, scale: Vec2) void { sgl.pushMatrix(); sgl.translate(translation.x, translation.y, 0); - sgl.scale(scale, scale, 1); + sgl.scale(scale.x, scale.y, 1); + + scale_stack.appendAssumeCapacity(scale_stack.getLast().multiply(scale)); } pub fn popTransform() void { sgl.popMatrix(); + _ = scale_stack.pop().?; } -pub fn drawQuad(quad: [4]Vec2, color: Vec4) void { +fn drawQuad(quad: [4]Vec2, color: Vec4) void { sgl.beginQuads(); defer sgl.end(); @@ -232,17 +309,7 @@ pub fn drawRectangle(pos: Vec2, size: Vec2, color: Vec4) void { ); } -pub fn drawTriangle(tri: [3]Vec2, color: Vec4) void { - sgl.beginTriangles(); - defer sgl.end(); - - sgl.c4f(color.x, color.y, color.z, color.w); - for (tri) |position| { - sgl.v2f(position.x, position.y); - } -} - -pub fn drawLine(from: Vec2, to: Vec2, color: Vec4, width: f32) void { +fn drawLine(from: Vec2, to: Vec2, color: Vec4, width: f32) void { const step = to.sub(from).normalized().multiplyScalar(width/2); const top_left = from.add(step.rotateLeft90()); @@ -256,55 +323,6 @@ pub fn drawLine(from: Vec2, to: Vec2, color: Vec4, width: f32) void { ); } -pub fn drawRectanglOutline(pos: Vec2, size: Vec2, color: Vec4, width: f32) void { - // TODO: Don't use line segments - drawLine(pos, pos.add(.{ .x = size.x, .y = 0 }), color, width); - drawLine(pos, pos.add(.{ .x = 0, .y = size.y }), color, width); - drawLine(pos.add(.{ .x = 0, .y = size.y }), pos.add(size), color, width); - drawLine(pos.add(.{ .x = size.x, .y = 0 }), pos.add(size), color, width); -} - -pub fn pushScissor(rect: Rect) void { - draw_frame.scissor_stack.appendAssumeCapacity(rect); - - sgl.scissorRectf(rect.pos.x, rect.pos.y, rect.size.x, rect.size.y, true); -} - -pub fn popScissor() void { - _ = draw_frame.scissor_stack.pop().?; - const rect = draw_frame.scissor_stack.getLast(); - - sgl.scissorRectf(rect.pos.x, rect.pos.y, rect.size.x, rect.size.y, true); -} - pub fn addFont(name: [*c]const u8, data: []const u8) !Font.Id { return try font_context.addFont(name, data); } - -pub const DrawTextOptions = struct { - font: Font.Id, - size: f32 = 16, - color: Vec4 = rgb(255, 255, 255), -}; - -pub fn drawText(position: Vec2, text: []const u8, opts: DrawTextOptions) void { - pushTransform(.{ .x = 0, .y = 0}, 1/font_resolution_scale); - defer popTransform(); - - font_context.setFont(opts.font); - font_context.setSize(opts.size * font_resolution_scale); - font_context.setAlign(.{ .x = .left, .y = .top }); - font_context.setSpacing(0); - - const r: u8 = @intFromFloat(opts.color.x * 255); - const g: u8 = @intFromFloat(opts.color.y * 255); - const b: u8 = @intFromFloat(opts.color.z * 255); - const a: u8 = @intFromFloat(opts.color.w * 255); - const color: u32 = r | (@as(u32, g) << 8) | (@as(u32, b) << 16) | (@as(u32, a) << 24); - font_context.setColor(color); - font_context.drawText( - position.x * font_resolution_scale, - position.y * font_resolution_scale, - text - ); -} diff --git a/src/engine/input.zig b/src/engine/input.zig index 99195f4..700c556 100644 --- a/src/engine/input.zig +++ b/src/engine/input.zig @@ -109,8 +109,3 @@ pub fn processEvent(frame: *Frame, event: Event) void { else => {} } } - -pub fn beginFrame(frame: *Frame) void { - frame.input.pressed_keys = .initEmpty(); - frame.input.released_keys = .initEmpty(); -} diff --git a/src/engine/root.zig b/src/engine/root.zig index 8664d1f..bc7b691 100644 --- a/src/engine/root.zig +++ b/src/engine/root.zig @@ -67,7 +67,7 @@ pub fn run(self: *Engine, opts: RunOptions) !void { self.allocator = std.heap.smp_allocator; } - self.frame = .init(self.allocator); + self.frame.init(self.allocator); tracy.setThreadName("Main"); @@ -147,36 +147,79 @@ fn sokolCleanup(self: *Engine) void { fn sokolFrame(self: *Engine) !void { tracy.frameMark(); - const time_passed = self.timePassed(); - defer self.last_frame_at = time_passed; - const dt_ns = time_passed - self.last_frame_at; - const zone = tracy.initZone(@src(), .{ }); defer zone.deinit(); - Gfx.beginFrame(); - defer Gfx.endFrame(); + const now = std.time.Instant.now() catch @panic("Instant.now() unsupported"); + const time_passed = now.since(self.started_at); + defer self.last_frame_at = time_passed; + + const frame = &self.frame; { - const window_size: Vec2 = .init(sapp.widthf(), sapp.heightf()); - const ctx = ScreenScalar.push(window_size, self.game.canvas_size); - defer ctx.pop(); + _ = frame.arena.reset(.retain_capacity); - Graphics.font_resolution_scale = ctx.scale; + const audio_commands_capacity = frame.audio.commands.capacity; + frame.audio = .empty; + try frame.audio.commands.ensureTotalCapacity(frame.arena.allocator(), audio_commands_capacity); + + const graphics_commands_capacity = frame.graphics.commands.capacity; + const scissor_stack_capacity = frame.graphics.scissor_stack.capacity; + frame.graphics = .empty; + frame.graphics.screen_size = .init(sapp.widthf(), sapp.heightf()); + try frame.graphics.commands.ensureTotalCapacity(frame.arena.allocator(), graphics_commands_capacity); + try frame.graphics.scissor_stack.ensureTotalCapacity(frame.arena.allocator(), scissor_stack_capacity); + frame.pushScissor(.init(0, 0, sapp.widthf(), sapp.heightf())); + + frame.time_ns = time_passed; + frame.dt_ns = time_passed - self.last_frame_at; - self.frame.time_ns = time_passed; - self.frame.dt_ns = dt_ns; try self.game.tick(&self.frame); + + frame.input.pressed_keys = .initEmpty(); + frame.input.released_keys = .initEmpty(); } - try self.game.debug(); + { + Gfx.beginFrame(); + defer Gfx.endFrame(frame.graphics.clear_color); - Input.beginFrame(&self.frame); + { + var screen_scaler: ?ScreenScalar = null; + if (self.frame.graphics.canvas_size) |canvas_size| { + screen_scaler = ScreenScalar.push(self.frame.graphics.screen_size, canvas_size); + } + + Gfx.drawCommands(frame.graphics.commands.items); + + if (screen_scaler) |ctx| ctx.pop(); + } + + if (frame.show_debug) { + try self.game.debug(); + try showDebugWindow(); + } + } + + for (frame.audio.commands.items) |command| { + try Audio.mixer.commands.push(command); + } } -fn timePassed(self: *Engine) Nanoseconds { - const now = std.time.Instant.now() catch @panic("Instant.now() unsupported"); - return now.since(self.started_at); +fn showDebugWindow() !void { + if (!imgui.beginWindow(.{ + .name = "Engine", + .pos = Vec2.init(240, 20), + .size = Vec2.init(200, 200), + })) { + return; + } + defer imgui.endWindow(); + + imgui.textFmt("Audio instances: {}/{}\n", .{ + Audio.mixer.instances.items.len, + Audio.mixer.instances.capacity + }); } fn sokolEvent(self: *Engine, e_ptr: [*c]const sapp.Event) !bool { diff --git a/src/engine/screen_scaler.zig b/src/engine/screen_scaler.zig index e7860f3..e795f93 100644 --- a/src/engine/screen_scaler.zig +++ b/src/engine/screen_scaler.zig @@ -26,7 +26,7 @@ pub fn push(window_size: Vec2, canvas_size: Vec2) ScreenScalar { translation.x = @round(translation.x); translation.y = @round(translation.y); - Gfx.pushTransform(translation, scale); + Gfx.pushTransform(translation, .init(scale, scale)); return ScreenScalar{ .window_size = window_size, diff --git a/src/game.zig b/src/game.zig index dbb5a1b..4f78a6c 100644 --- a/src/game.zig +++ b/src/game.zig @@ -8,14 +8,11 @@ const Engine = @import("./engine/root.zig"); const imgui = Engine.imgui; const Vec2 = Engine.Vec2; const rgb = Engine.Math.rgb; -const Gfx = Engine.Graphics; -const Audio = Engine.Audio; const Game = @This(); gpa: Allocator, assets: *Assets, -canvas_size: Vec2, player: Vec2, @@ -23,7 +20,6 @@ pub fn init(gpa: Allocator, assets: *Assets) !Game { return Game{ .gpa = gpa, .assets = assets, - .canvas_size = Vec2.init(100, 100), .player = .init(50, 50) }; } @@ -35,6 +31,13 @@ pub fn deinit(self: *Game) void { pub fn tick(self: *Game, frame: *Engine.Frame) !void { const dt = frame.deltaTime(); + const canvas_size = Vec2.init(100, 100); + frame.graphics.canvas_size = canvas_size; + + if (frame.isKeyPressed(.F3)) { + frame.show_debug = !frame.show_debug; + } + var dir = Vec2.init(0, 0); if (frame.isKeyDown(.W)) { dir.y -= 1; @@ -60,14 +63,14 @@ pub fn tick(self: *Game, frame: *Engine.Frame) !void { const regular_font = self.assets.font_id.get(.regular); - Gfx.drawRectangle(.init(0, 0), self.canvas_size, rgb(20, 20, 20)); + frame.drawRectangle(.init(0, 0), canvas_size, rgb(20, 20, 20)); const size = Vec2.init(20, 20); - Gfx.drawRectangle(self.player.sub(size.divideScalar(2)), size, rgb(200, 20, 20)); + frame.drawRectangle(self.player.sub(size.divideScalar(2)), size, rgb(200, 20, 20)); if (dir.x != 0 or dir.y != 0) { - Gfx.drawRectanglOutline(self.player.sub(size.divideScalar(2)), size, rgb(20, 200, 20), 3); + frame.drawRectanglOutline(self.player.sub(size.divideScalar(2)), size, rgb(20, 200, 20), 3); } - Gfx.drawText(self.player, "Player", .{ + frame.drawText(self.player, "Player", .{ .font = regular_font, .size = 10 }); @@ -76,17 +79,13 @@ pub fn tick(self: *Game, frame: *Engine.Frame) !void { pub fn debug(self: *Game) !void { _ = self; // autofix if (!imgui.beginWindow(.{ - .name = "Debug", + .name = "Game", .pos = Vec2.init(20, 20), - .size = Vec2.init(400, 200), + .size = Vec2.init(200, 200), })) { return; } defer imgui.endWindow(); imgui.text("Hello World!\n"); - imgui.textFmt("Audio: {}/{}\n", .{ - Audio.mixer.instances.items.len, - Audio.mixer.instances.capacity - }); }