From 9d733902b54750e769a52609ad358ae98876ae72 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Sun, 8 Feb 2026 01:15:46 +0200 Subject: [PATCH] add game code loading --- build.zig | 2 +- engine/build.zig | 24 ++++++++-- engine/src/runtime/main.zig | 28 ++++++++++- engine/src/runtime/root.zig | 96 +++++++++++++++++++++++++------------ 4 files changed, 113 insertions(+), 37 deletions(-) diff --git a/build.zig b/build.zig index 25b00a8..2fe6ba6 100644 --- a/build.zig +++ b/build.zig @@ -20,7 +20,7 @@ pub fn build(b: *std.Build) !void { .has_imgui = b.option(bool, "imgui", "ImGui integration"), .has_tracy = b.option(bool, "tracy", "Tracy integration"), .win32_has_console = b.option(bool, "console", "Show console (Window only)"), - .win32_png_icon = b.path("src/assets/icon.png") + .win32_png_icon = b.path("src/assets/icon.png"), }); { diff --git a/engine/build.zig b/engine/build.zig index 96b59ad..b3b6783 100644 --- a/engine/build.zig +++ b/engine/build.zig @@ -14,6 +14,7 @@ const InitOptions = struct { dep_engine: *std.Build.Dependency, + hot_reload: ?bool = null, has_imgui: ?bool = null, has_tracy: ?bool = null, win32_has_console: ?bool = null, @@ -23,12 +24,19 @@ const InitOptions = struct { pub fn init(outer_builder: *std.Build, opts: InitOptions) void { const b = opts.dep_engine.builder; + const isWasm = opts.target.result.cpu.arch.isWasm(); + + var hot_reload = false; + if (!isWasm) { + hot_reload = opts.hot_reload orelse (opts.optimize == .Debug); + } + opts.root_module.resolved_target = opts.target; opts.root_module.optimize = opts.optimize; const game_lib = b.addLibrary(.{ .name = "game", .root_module = opts.root_module, - .linkage = .static, + .linkage = if (hot_reload) .dynamic else .static, }); const engine_lib = b.createModule(.{ .root_source_file = b.path("src/lib/root.zig") @@ -39,7 +47,8 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void { .target = opts.target, .optimize = opts.optimize, .has_tracy = opts.has_tracy, - .has_imgui = opts.has_imgui + .has_imgui = opts.has_imgui, + .hot_reload = hot_reload }); const mod_main = b.createModule(.{ @@ -50,10 +59,12 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void { }); mod_main.addImport("lib", engine_lib); mod_main.addImport("engine", engine_module.root_module); - mod_main.linkLibrary(game_lib); + if (!hot_reload) { + mod_main.linkLibrary(game_lib); + } var run_cmd_step: *std.Build.Step = undefined; - if (opts.target.result.cpu.arch.isWasm()) { + if (isWasm) { const build_step = buildWasm(b, .{ .name = "index", .root_module = mod_main, @@ -83,6 +94,9 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void { if (outer_builder.args) |args| { run_cmd.addArgs(args); } + if (hot_reload) { + run_cmd.addFileArg(game_lib.getEmittedBin()); + } run_cmd_step = &run_cmd.step; } } @@ -101,6 +115,7 @@ const EngineModule = struct { has_imgui: ?bool = null, has_tracy: ?bool = null, + hot_reload: bool }; }; @@ -197,6 +212,7 @@ fn createEngineModule(b: *std.Build, opts: EngineModule.Options) EngineModule { var options = b.addOptions(); options.addOption(bool, "has_imgui", has_imgui); options.addOption(bool, "has_tracy", has_tracy); + options.addOption(bool, "hot_reload", opts.hot_reload); mod.addOptions("build_options", options); return EngineModule{ diff --git a/engine/src/runtime/main.zig b/engine/src/runtime/main.zig index 6cf0834..58a6b94 100644 --- a/engine/src/runtime/main.zig +++ b/engine/src/runtime/main.zig @@ -1,8 +1,34 @@ +const std = @import("std"); +const builtin = @import("builtin"); const Engine = @import("engine"); var engine: Engine = undefined; pub fn main() !void { - try engine.run(.{}); + var debug_allocator: std.heap.DebugAllocator(.{}) = .init; + defer _ = debug_allocator.deinit(); + + // TODO: Use tracy TracingAllocator + var allocator: std.mem.Allocator = undefined; + if (builtin.cpu.arch.isWasm()) { + allocator = std.heap.wasm_allocator; + } else if (builtin.mode == .Debug) { + allocator = debug_allocator.allocator(); + } else { + allocator = std.heap.smp_allocator; + } + + const args = try std.process.argsAlloc(allocator); + defer std.process.argsFree(allocator, args); + + var game_library_path: ?[]const u8 = null; + if (args.len >= 2) { + game_library_path = args[1]; + } + + try engine.run(.{ + .allocator = allocator, + .game_library_path = game_library_path + }); } diff --git a/engine/src/runtime/root.zig b/engine/src/runtime/root.zig index 7661c42..6ed6503 100644 --- a/engine/src/runtime/root.zig +++ b/engine/src/runtime/root.zig @@ -17,39 +17,72 @@ const STBImage = @import("stb_image"); const Gfx = Graphics; +const build_options = @import("build_options"); + const Lib = @import("lib"); pub const Math = Lib.Math; pub const Vec2 = Math.Vec2; const rgb = Math.rgb; const GameCallbacks = struct { - init: *const fn(opts: *const Lib.Init) callconv(.c) void, - tick: *const fn(frame: *Lib.Frame) callconv(.c) void, - deinit: *const fn() callconv(.c) void, - debug: *const fn(imgui: *Lib.ImGui) callconv(.c) void, + const Init = *const fn(opts: *const Lib.Init) callconv(.c) void; + const Tick = *const fn(frame: *Lib.Frame) callconv(.c) void; + const Deinit = *const fn() callconv(.c) void; + const Debug = *const fn(imgui: *Lib.ImGui) callconv(.c) void; + + init: Init, + tick: Tick, + deinit: Deinit, + debug: if (build_options.has_imgui) Debug else void }; extern fn init(opts: *const Lib.Init) void; extern fn tick(frame: *Lib.Frame) void; extern fn deinit() void; extern fn debug(imgui: *Lib.ImGui) void; -const static_game_callbacks = GameCallbacks{ - .init = init, - .deinit = deinit, - .tick = tick, - .debug = debug, -}; +const GameLinking = union(enum) { + static, + dynamic: struct { + lib: std.DynLib + }, + + pub fn getCallbacks(self: *GameLinking) !GameCallbacks { + switch (self.*) { + .static => { + if (build_options.hot_reload) { + return error.StaticLinkingDisabled; + } + + return GameCallbacks{ + .init = init, + .deinit = deinit, + .tick = tick, + .debug = if (build_options.has_imgui) debug else {}, + }; + }, + .dynamic => |*opts| { + const lib = &opts.lib; + return GameCallbacks{ + .init = lib.lookup(GameCallbacks.Init, "init") orelse return error.MissingFunction, + .tick = lib.lookup(GameCallbacks.Tick, "tick") orelse return error.MissingFunction, + .deinit = lib.lookup(GameCallbacks.Deinit, "deinit") orelse return error.MissingFunction, + .debug = if (build_options.has_imgui) lib.lookup(GameCallbacks.Debug, "debug") orelse return error.MissingFunction else {}, + }; + } + } + } +}; const Engine = @This(); -pub const Nanoseconds = u64; - allocator: std.mem.Allocator, started_at: std.time.Instant, -last_frame_at: Nanoseconds, +last_frame_at: Lib.Nanoseconds, graphics: Graphics, input: Input, + +game_linking: GameLinking, game_callbacks: GameCallbacks, frame: Lib.Frame, @@ -57,34 +90,34 @@ frame: Lib.Frame, previous_tick_canvas_size: ?Vec2 = null, const RunOptions = struct { - window_title: [*:0]const u8 = "Game", - window_width: u31 = 640, - window_height: u31 = 480, + allocator: std.mem.Allocator, + game_library_path: ?[]const u8 = null }; pub fn run(self: *Engine, opts: RunOptions) !void { self.* = Engine{ - .allocator = undefined, + .allocator = opts.allocator, .started_at = std.time.Instant.now() catch @panic("Instant.now() unsupported"), .last_frame_at = 0, .frame = undefined, .graphics = undefined, - .game_callbacks = static_game_callbacks, + .game_linking = undefined, + .game_callbacks = undefined, .input = .{} }; - var debug_allocator: std.heap.DebugAllocator(.{}) = .init; - defer _ = debug_allocator.deinit(); - - // TODO: Use tracy TracingAllocator - if (builtin.cpu.arch.isWasm()) { - self.allocator = std.heap.wasm_allocator; - } else if (builtin.mode == .Debug) { - self.allocator = debug_allocator.allocator(); + if (opts.game_library_path) |game_library_path| { + self.game_linking = .{ + .dynamic = .{ + .lib = try std.DynLib.open(game_library_path) + } + }; } else { - self.allocator = std.heap.smp_allocator; + self.game_linking = .static; } + self.game_callbacks = try self.game_linking.getCallbacks(); + self.frame.init(self.allocator); tracy.setThreadName("Main"); @@ -120,10 +153,10 @@ pub fn run(self: *Engine, opts: RunOptions) !void { .cleanup_userdata_cb = sokolCleanupCallback, .event_userdata_cb = sokolEventCallback, .user_data = self, - .width = opts.window_width, - .height = opts.window_height, + .width = 640, + .height = 480, .icon = icon, - .window_title = opts.window_title, + .window_title = "Game", .logger = .{ .func = sokolLogCallback }, .win32 = .{ .console_utf8 = true @@ -238,7 +271,7 @@ fn sokolFrame(self: *Engine) !void { self.graphics.drawCommands(frame.graphics_commands.items); - if (frame.show_debug) { + if (frame.show_debug and build_options.has_imgui) { try self.showDebugWindow(frame); } } @@ -259,6 +292,7 @@ fn showDebugWindow(self: *Engine, frame: *Lib.Frame) !void { defer imgui.endWindow(); _ = imgui.beginTabBar("debug"); + defer imgui.endTabBar(); if (imgui.beginTabItem("Game")) { defer imgui.endTabItem();