separate frame specific data

This commit is contained in:
Rokas Puzonas 2026-01-22 01:15:19 +02:00
parent 4283dd9926
commit acaf58cf3c
6 changed files with 329 additions and 135 deletions

View File

@ -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 = {}
});
}

View File

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

View File

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

View File

@ -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 {

View File

@ -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,

View File

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