From c2e784bfb21dea81280f28f261ea1ebb9fbf1622 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Sat, 13 Dec 2025 23:47:35 +0200 Subject: [PATCH] add canvas scaling --- src/game.zig | 78 +++++++++++++ src/graphics.zig | 10 ++ src/main.zig | 224 ++----------------------------------- src/window.zig | 280 ++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 371 insertions(+), 221 deletions(-) create mode 100644 src/game.zig diff --git a/src/game.zig b/src/game.zig new file mode 100644 index 0000000..6fcb56c --- /dev/null +++ b/src/game.zig @@ -0,0 +1,78 @@ +const std = @import("std"); + +const Math = @import("./math.zig"); +const Vec2 = Math.Vec2; +const rgb = Math.rgb; + +const Window = @import("./window.zig"); + +const imgui = @import("./imgui.zig"); + +const Gfx = @import("./graphics.zig"); + +const Game = @This(); + +pub const Input = struct { + dt: f32, + move: Vec2 +}; + +canvas_size: Vec2, +position: Vec2 = .init(0, 0), + +pub fn init() !Game { + return Game{ + .canvas_size = .init(320, 180) + }; +} + +pub fn deinit(self: *Game) void { + _ = self; // autofix +} + +pub fn getInput(self: *Game, window: *Window) Input { + _ = self; // autofix + const dt = @as(f32, @floatFromInt(window.frame_dt_ns)) / std.time.ns_per_s; + + var move = Vec2.init(0, 0); + if (window.down_keys.contains(.W)) { + move.y -= 1; + } + if (window.down_keys.contains(.S)) { + move.y += 1; + } + if (move.x == 0) { + if (window.down_keys.contains(.A)) { + move.x -= 1; + } + if (window.down_keys.contains(.D)) { + move.x += 1; + } + } + + return Input{ + .dt = dt, + .move = move + }; +} + +pub fn tick(self: *Game, input: Input) !void { + + const velocity = input.move.multiplyScalar(100); + self.position = self.position.add(velocity.multiplyScalar(input.dt)); + + Gfx.drawRectangle(.init(0, 0), .init(self.canvas_size.x, self.canvas_size.y), rgb(255, 255, 255)); + Gfx.drawRectangle(self.position, .init(10, 10), rgb(255, 0, 0)); +} + +pub fn debug(self: *Game) !void { + _ = self; // autofix + if (!imgui.beginWindow(.{ + .name = "Debug", + .pos = Vec2.init(20, 20), + .size = Vec2.init(400, 200), + })) { + return; + } + defer imgui.endWindow(); +} diff --git a/src/graphics.zig b/src/graphics.zig index 76e32d1..036f6f5 100644 --- a/src/graphics.zig +++ b/src/graphics.zig @@ -1297,6 +1297,16 @@ pub fn popScissor() void { sgl.scissorRectf(rect.pos.x, rect.pos.y, rect.size.x, rect.size.y, true); } +pub fn pushTransform(translation: Vec2, scale: f32) void { + sgl.pushMatrix(); + sgl.translate(translation.x, translation.y, 0); + sgl.scale(scale, scale, 1); +} + +pub fn popTransform() void { + sgl.popMatrix(); +} + pub fn event(ev: [*c]const sapp.Event) bool { return imgui.handleEvent(ev.*); } diff --git a/src/main.zig b/src/main.zig index fa13212..736a337 100644 --- a/src/main.zig +++ b/src/main.zig @@ -16,97 +16,35 @@ const sg = sokol.gfx; const sapp = sokol.app; const sglue = sokol.glue; -const log = std.log.scoped(.ui); +const log = std.log.scoped(.main); + +var gpa: std.mem.Allocator = undefined; var window: Window = undefined; -var event_queue: std.ArrayListUnmanaged(Event) = .empty; -var mouse_inside_window: bool = false; var event_queue_full_shown = false; -var sokol_logger: sapp.Logger = .{ .func = sokolLogCallback }; fn signalHandler(sig: i32) callconv(.c) void { _ = sig; sapp.requestQuit(); } -fn initErrorable() !void { - // const allocator = window.app.allocator; - - try Gfx.init(.{ - .allocator = window.gpa, - .logger = @as(*sokol.gfx.Logger, @ptrFromInt(@intFromPtr(&sokol_logger))).*, - }); -} - export fn init() void { var zone = tracy.initZone(@src(), .{ }); defer zone.deinit(); - initErrorable() catch |e| { + Window.init(&window, gpa) catch |e| { log.err("init() failed: {}", .{e}); sapp.requestQuit(); }; } -fn cStrToZig(c_str: [*c]const u8) [:0]const u8 { - return @import("std").mem.span(c_str); -} - -fn sokolLogFmt(log_level: u32, comptime format: []const u8, args: anytype) void { - const log_sokol = std.log.scoped(.sokol); - - if (log_level == 0) { - log_sokol.err(format, args); - } else if (log_level == 1) { - log_sokol.err(format, args); - } else if (log_level == 2) { - log_sokol.warn(format, args); - } else { - log_sokol.info(format, args); - } -} - -fn sokolLogCallback(tag: [*c]const u8, log_level: u32, log_item: u32, message: [*c]const u8, line_nr: u32, filename: [*c]const u8, user_data: ?*anyopaque) callconv(.c) void { - _ = user_data; - - if (filename != null) { - const format = "[{s}][id:{}] {s}:{}: {s}"; - const args = .{ - cStrToZig(tag orelse "-"), - log_item, - std.fs.path.basename(cStrToZig(filename orelse "-")), - line_nr, - cStrToZig(message orelse "") - }; - - sokolLogFmt(log_level, format, args); - } else { - const format = "[{s}][id:{}] {s}"; - const args = .{ - cStrToZig(tag orelse "-"), - log_item, - cStrToZig(message orelse "") - }; - - sokolLogFmt(log_level, format, args); - } -} - -fn frameErrorable() !void { - Gfx.beginFrame(); - defer Gfx.endFrame(); - - // try window.frame(event_queue.items); - event_queue.clearRetainingCapacity(); -} - export fn frame() void { tracy.frameMark(); const zone = tracy.initZone(@src(), .{ }); defer zone.deinit(); - frameErrorable() catch |e| { + window.frame() catch |e| { log.err("frame() failed: {}", .{e}); if (@errorReturnTrace()) |trace| { std.debug.dumpStackTrace(trace.*); @@ -119,152 +57,14 @@ export fn cleanup() void { const zone = tracy.initZone(@src(), .{ }); defer zone.deinit(); - // window.beforeExit() catch |e| { - // log.err("window.beforeExit() failed: {}", .{e}); - // if (@errorReturnTrace()) |trace| { - // std.debug.dumpStackTrace(trace.*); - // } - // }; - - // const allocator = window.app.allocator; - window.deinit(); - Gfx.deinit(); - // event_queue.deinit(allocator); -} - -fn appendToEventQueue(e_ptr: [*c]const sapp.Event) !bool { - const e = e_ptr.*; - blk: switch (e.type) { - .MOUSE_DOWN => { - const mouse_button = Window.MouseButton.fromSokol(e.mouse_button) orelse break :blk; - - try appendEvent(Event{ - .mouse_pressed = .{ - .button = mouse_button, - .position = Vec2.init(e.mouse_x, e.mouse_y) - } - }); - - return true; - }, - .MOUSE_UP => { - const mouse_button = MouseButton.fromSokol(e.mouse_button) orelse break :blk; - - try appendEvent(Event{ - .mouse_released = .{ - .button = mouse_button, - .position = Vec2.init(e.mouse_x, e.mouse_y) - } - }); - - return true; - }, - .MOUSE_MOVE => { - if (!mouse_inside_window) { - try appendEvent(Event{ - .mouse_enter = Vec2.init(e.mouse_x, e.mouse_y) - }); - } - - try appendEvent(Event{ - .mouse_move = Vec2.init(e.mouse_x, e.mouse_y) - }); - - mouse_inside_window = true; - return true; - }, - .MOUSE_ENTER => { - if (!mouse_inside_window) { - try appendEvent(Event{ - .mouse_enter = Vec2.init(e.mouse_x, e.mouse_y) - }); - } - - mouse_inside_window = true; - return true; - }, - .RESIZED => { - if (mouse_inside_window) { - try appendEvent(Event{ - .mouse_leave = {} - }); - } - - try appendEvent(Event{ - .window_resize = {} - }); - - mouse_inside_window = false; - return true; - }, - .MOUSE_LEAVE => { - if (mouse_inside_window) { - try appendEvent(Event{ - .mouse_leave = {} - }); - } - - mouse_inside_window = false; - return true; - }, - .MOUSE_SCROLL => { - try appendEvent(Event{ - .mouse_scroll = Vec2.init(e.scroll_x, e.scroll_y) - }); - - return true; - }, - .KEY_DOWN => { - try appendEvent(Event{ - .key_pressed = .{ - .code = @enumFromInt(@intFromEnum(e.key_code)), - .repeat = e.key_repeat - } - }); - - return true; - }, - .KEY_UP => { - try appendEvent(Event{ - .key_released = @enumFromInt(@intFromEnum(e.key_code)) - }); - - return true; - }, - .CHAR => { - try appendEvent(Event{ - .char = @intCast(e.char_code) - }); - - return true; - }, - .QUIT_REQUESTED => { - // TODO: handle quit request. Maybe show confirmation window in certain cases. - }, - else => {} - } - - return false; -} - -fn appendEvent(e: Event) !void { - event_queue.appendBounded(e) catch return error.EventQueueFull; } export fn event(e_ptr: [*c]const sapp.Event) void { const zone = tracy.initZone(@src(), .{ }); defer zone.deinit(); - if (Gfx.event(e_ptr)) { - appendEvent(Event{ - .mouse_leave = {} - }) catch {}; - mouse_inside_window = false; - return; - } - - const consumed_event = appendToEventQueue(e_ptr) catch |e| switch (e) { + const consumed_event = window.event(e_ptr) catch |e| switch (e) { error.EventQueueFull => blk: { if (!event_queue_full_shown) { log.warn("Event queue is full! Frame is taking too long to process", .{}); @@ -272,13 +72,6 @@ export fn event(e_ptr: [*c]const sapp.Event) void { } break :blk false; }, - // else => blk: { - // log.err("Failed to append event to queue: {}", .{e}); - // if (@errorReturnTrace()) |trace| { - // std.debug.dumpStackTrace(trace.*); - // } - // break :blk false; - // } }; if (consumed_event) { @@ -291,7 +84,6 @@ pub fn main() !void { var debug_allocator: std.heap.DebugAllocator(.{}) = .init; defer _ = debug_allocator.deinit(); - var gpa: std.mem.Allocator = undefined; if (builtin.mode == .ReleaseFast) { gpa = std.heap.smp_allocator; } else { @@ -302,8 +94,6 @@ pub fn main() !void { tracy.setThreadName("Main"); - try Window.init(&window, gpa); - var sa: std.posix.Sigaction = .{ .handler = .{ .handler = signalHandler }, .mask = std.posix.sigemptyset(), @@ -322,7 +112,7 @@ pub fn main() !void { .high_dpi = true, .sample_count = 4, .window_title = "Game", - .logger = sokol_logger, + .logger = .{ .func = Window.sokolLogCallback }, }); } diff --git a/src/window.zig b/src/window.zig index 79685e3..a079609 100644 --- a/src/window.zig +++ b/src/window.zig @@ -2,12 +2,20 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const sokol = @import("sokol"); +const sapp = sokol.app; + +const Gfx = @import("./graphics.zig"); const Math = @import("./math.zig"); const Vec2 = Math.Vec2; +const rgb = Math.rgb; + +const Game = @import("./game.zig"); const Window = @This(); +const log = std.log.scoped(.window); + pub const MouseButton = enum { left, right, @@ -170,18 +178,282 @@ pub const Event = union(enum) { gpa: Allocator, events: std.ArrayList(Event), +mouse_inside: bool = false, +last_frame_at_ns: i128, +frame_dt_ns: i128, +down_keys: std.EnumSet(KeyCode) = .initEmpty(), +game: Game, pub fn init(self: *Window, gpa: Allocator) !void { + var events: std.ArrayList(Event) = .empty; + errdefer events.deinit(gpa); + try events.ensureTotalCapacityPrecise(gpa, 50); + + try Gfx.init(.{ + .allocator = gpa, + .logger = .{ .func = sokolLogCallback } + }); + + var game = try Game.init(); + errdefer game.deinit(); + self.* = Window{ .gpa = gpa, - .events = .empty - // .last_frame_at_ns = std.time.nanoTimestamp(), - // .frame_arena = ArenaAllocator.init(gpa), + .events = events, + .last_frame_at_ns = std.time.nanoTimestamp(), + .frame_dt_ns = 0, + .game = game }; } pub fn deinit(self: *Window) void { const gpa = self.gpa; - _ = gpa; // autofix + self.game.deinit(); + self.events.deinit(gpa); + + Gfx.deinit(); +} + +pub fn frame(self: *Window) !void { + const now = std.time.nanoTimestamp(); + self.frame_dt_ns = now - self.last_frame_at_ns; + self.last_frame_at_ns = now; + + Gfx.beginFrame(); + defer Gfx.endFrame(); + + for (self.events.items) |e| { + switch (e) { + .key_pressed => |opts| { + if (!opts.repeat) { + self.down_keys.insert(opts.code); + } + }, + .key_released => |key_code| { + self.down_keys.remove(key_code); + }, + .mouse_leave => { + self.down_keys = .initEmpty(); + }, + else => {} + } + } + self.events.clearRetainingCapacity(); + + const window_size: Vec2 = .init(sapp.widthf(), sapp.heightf()); + const scale = @min( + window_size.x / self.game.canvas_size.x, + window_size.y / self.game.canvas_size.y, + ); + + const filler_size: Vec2 = Vec2.sub(window_size, self.game.canvas_size.multiplyScalar(scale)).multiplyScalar(0.5); + + const input = self.game.getInput(self); + + { + Gfx.pushTransform(filler_size, scale); + defer Gfx.popTransform(); + + try self.game.tick(input); + } + + const bg_color = rgb(0, 0, 0); + + Gfx.drawRectangle( + .init(0, 0), + .init(window_size.x, filler_size.y), + bg_color + ); + + Gfx.drawRectangle( + .init(0, window_size.y - filler_size.y), + .init(window_size.x, filler_size.y), + bg_color + ); + + Gfx.drawRectangle( + .init(0, 0), + .init(filler_size.x, window_size.y), + bg_color + ); + + Gfx.drawRectangle( + .init(window_size.x - filler_size.x, 0), + .init(filler_size.x, window_size.y), + bg_color + ); + + try self.game.debug(); +} + +fn appendEvent(self: *Window, e: Event) !void { + self.events.appendBounded(e) catch return error.EventQueueFull; +} + +pub fn event(self: *Window, e_ptr: [*c]const sapp.Event) !bool { + if (Gfx.event(e_ptr)) { + try self.appendEvent(Event{ + .mouse_leave = {} + }); + self.mouse_inside = false; + return true; + } + + const e = e_ptr.*; + blk: switch (e.type) { + .MOUSE_DOWN => { + const mouse_button = Window.MouseButton.fromSokol(e.mouse_button) orelse break :blk; + + try self.appendEvent(Event{ + .mouse_pressed = .{ + .button = mouse_button, + .position = Vec2.init(e.mouse_x, e.mouse_y) + } + }); + + return true; + }, + .MOUSE_UP => { + const mouse_button = MouseButton.fromSokol(e.mouse_button) orelse break :blk; + + try self.appendEvent(Event{ + .mouse_released = .{ + .button = mouse_button, + .position = Vec2.init(e.mouse_x, e.mouse_y) + } + }); + + return true; + }, + .MOUSE_MOVE => { + if (!self.mouse_inside) { + try self.appendEvent(Event{ + .mouse_enter = Vec2.init(e.mouse_x, e.mouse_y) + }); + } + + try self.appendEvent(Event{ + .mouse_move = Vec2.init(e.mouse_x, e.mouse_y) + }); + + self.mouse_inside = true; + return true; + }, + .MOUSE_ENTER => { + if (!self.mouse_inside) { + try self.appendEvent(Event{ + .mouse_enter = Vec2.init(e.mouse_x, e.mouse_y) + }); + } + + self.mouse_inside = true; + return true; + }, + .RESIZED => { + if (self.mouse_inside) { + try self.appendEvent(Event{ + .mouse_leave = {} + }); + } + + try self.appendEvent(Event{ + .window_resize = {} + }); + + self.mouse_inside = false; + return true; + }, + .MOUSE_LEAVE => { + if (self.mouse_inside) { + try self.appendEvent(Event{ + .mouse_leave = {} + }); + } + + self.mouse_inside = false; + return true; + }, + .MOUSE_SCROLL => { + try self.appendEvent(Event{ + .mouse_scroll = Vec2.init(e.scroll_x, e.scroll_y) + }); + + return true; + }, + .KEY_DOWN => { + try self.appendEvent(Event{ + .key_pressed = .{ + .code = @enumFromInt(@intFromEnum(e.key_code)), + .repeat = e.key_repeat + } + }); + + return true; + }, + .KEY_UP => { + try self.appendEvent(Event{ + .key_released = @enumFromInt(@intFromEnum(e.key_code)) + }); + + return true; + }, + .CHAR => { + try self.appendEvent(Event{ + .char = @intCast(e.char_code) + }); + + return true; + }, + .QUIT_REQUESTED => { + // TODO: handle quit request. Maybe show confirmation window in certain cases. + }, + else => {} + } + + return false; +} + +fn cStrToZig(c_str: [*c]const u8) [:0]const u8 { + return std.mem.span(c_str); +} + +fn sokolLogFmt(log_level: u32, comptime format: []const u8, args: anytype) void { + const log_sokol = std.log.scoped(.sokol); + + if (log_level == 0) { + log_sokol.err(format, args); + } else if (log_level == 1) { + log_sokol.err(format, args); + } else if (log_level == 2) { + log_sokol.warn(format, args); + } else { + log_sokol.info(format, args); + } +} + +pub fn sokolLogCallback(tag: [*c]const u8, log_level: u32, log_item: u32, message: [*c]const u8, line_nr: u32, filename: [*c]const u8, user_data: ?*anyopaque) callconv(.c) void { + _ = user_data; + + if (filename != null) { + const format = "[{s}][id:{}] {s}:{}: {s}"; + const args = .{ + cStrToZig(tag orelse "-"), + log_item, + std.fs.path.basename(cStrToZig(filename orelse "-")), + line_nr, + cStrToZig(message orelse "") + }; + + sokolLogFmt(log_level, format, args); + } else { + const format = "[{s}][id:{}] {s}"; + const args = .{ + cStrToZig(tag orelse "-"), + log_item, + cStrToZig(message orelse "") + }; + + sokolLogFmt(log_level, format, args); + } }