From 407da9e0094796ef502e636cc551a5b143ba2af3 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Sun, 8 Feb 2026 21:55:59 +0200 Subject: [PATCH] add hot reload --- build.zig | 1 + engine/build.zig | 91 ++-- engine/src/lib/root.zig | 40 +- engine/src/runtime/audio/root.zig | 2 +- engine/src/runtime/engine.zig | 788 ++++++++++++++++++++++++++++++ engine/src/runtime/imgui.zig | 4 +- engine/src/runtime/input.zig | 8 +- engine/src/runtime/main.zig | 2 +- engine/src/runtime/root.zig | 539 -------------------- src/game.zig | 41 +- 10 files changed, 898 insertions(+), 618 deletions(-) create mode 100644 engine/src/runtime/engine.zig delete mode 100644 engine/src/runtime/root.zig diff --git a/build.zig b/build.zig index 2fe6ba6..644d8f5 100644 --- a/build.zig +++ b/build.zig @@ -21,6 +21,7 @@ pub fn build(b: *std.Build) !void { .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"), + .hot_reload = b.option(bool, "hot-reload", "Dynamically load game code at runtime"), }); { diff --git a/engine/build.zig b/engine/build.zig index 371da9a..ee5d44e 100644 --- a/engine/build.zig +++ b/engine/build.zig @@ -14,6 +14,7 @@ const InitOptions = struct { dep_engine: *std.Build.Dependency, + statically_linked: ?bool = null, hot_reload: ?bool = null, has_imgui: ?bool = null, has_tracy: ?bool = null, @@ -26,57 +27,61 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void { const isWasm = opts.target.result.cpu.arch.isWasm(); + var has_tracy = false; var hot_reload = false; if (!isWasm) { hot_reload = opts.hot_reload orelse (opts.optimize == .Debug); + has_tracy = opts.has_tracy orelse (opts.optimize == .Debug); } + const has_imgui = opts.has_imgui orelse (opts.optimize == .Debug); + const statically_linked = opts.statically_linked orelse !hot_reload; + + var build_options = b.addOptions(); + build_options.addOption(bool, "has_imgui", has_imgui); + build_options.addOption(bool, "has_tracy", has_tracy); + build_options.addOption(bool, "hot_reload", hot_reload); + build_options.addOption(bool, "statically_linked", statically_linked); + const engine_lib = b.createModule(.{ .root_source_file = b.path("src/lib/root.zig") }); + engine_lib.addOptions("build_options", build_options); opts.root_module.addImport("engine", engine_lib); - const engine_module = createEngineModule(b, .{ + const runtime_module = createRuntimeModule(b, .{ .target = opts.target, .optimize = opts.optimize, - .has_tracy = opts.has_tracy, - .has_imgui = opts.has_imgui, - .hot_reload = hot_reload + .has_tracy = has_tracy, + .has_imgui = has_imgui }); - engine_module.root_module.addImport("lib", engine_lib); - if (!hot_reload) { - engine_module.root_module.addImport("game", opts.root_module); + runtime_module.root_module.addImport("lib", engine_lib); + if (statically_linked) { + runtime_module.root_module.addImport("game", opts.root_module); } - const mod_main = b.createModule(.{ - .root_source_file = b.path("src/runtime/main.zig"), - .target = opts.target, - .optimize = opts.optimize, - .link_libc = true - }); - mod_main.addImport("engine", engine_module.root_module); - var run_cmd_step: *std.Build.Step = undefined; if (isWasm) { const build_step = buildWasm(b, .{ .name = "index", - .root_module = mod_main, - .dep_sokol = engine_module.dep_sokol, + .root_module = runtime_module.root_module, + .dep_sokol = runtime_module.dep_sokol, .outer_builder = outer_builder }); outer_builder.getInstallStep().dependOn(build_step); - const dep_sokol = engine_module.dep_sokol; + const dep_sokol = runtime_module.dep_sokol; const dep_emsdk = dep_sokol.builder.dependency("emsdk", .{}); - const emrun_step = sokol.emRunStep(b, .{ .name = "index", .emsdk = dep_emsdk }); + const emrun_step = sokol.emRunStep(outer_builder, .{ .name = "index", .emsdk = dep_emsdk }); emrun_step.step.dependOn(build_step); run_cmd_step = &emrun_step.step; // TODO: Create a zip archive of all of the files. Would be useful for easier itch.io upload } else { + runtime_module.root_module.link_libc = true; const exe = buildNative(b, .{ .name = opts.name, - .root_module = mod_main, + .root_module = runtime_module.root_module, .win32_has_console = opts.win32_has_console, .win32_png_icon = opts.win32_png_icon }); @@ -93,10 +98,15 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void { const game_lib = b.addLibrary(.{ .name = "game", .root_module = opts.root_module, - .linkage = .dynamic + .linkage = .dynamic, }); + const install_game_lib = b.addInstallArtifact(game_lib, .{}); - run_cmd.addFileArg(game_lib.getEmittedBin()); + run_cmd.addArg(b.getInstallPath(.lib, game_lib.out_lib_filename)); + run_cmd.step.dependOn(&install_game_lib.step); + + var game_lib_step = outer_builder.step("game-lib", "Build game dynamic library"); + game_lib_step.dependOn(&install_game_lib.step); } run_cmd_step = &run_cmd.step; } @@ -106,7 +116,7 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void { run_step.dependOn(run_cmd_step); } -const EngineModule = struct { +const RuntimeModule = struct { root_module: *std.Build.Module, dep_sokol: *std.Build.Dependency, @@ -114,23 +124,14 @@ const EngineModule = struct { target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, - has_imgui: ?bool = null, - has_tracy: ?bool = null, - hot_reload: bool + has_imgui: bool, + has_tracy: bool, }; }; -fn createEngineModule(b: *std.Build, opts: EngineModule.Options) EngineModule { - const has_imgui = opts.has_imgui orelse (opts.optimize == .Debug); - var has_tracy = opts.has_tracy orelse (opts.optimize == .Debug); - const isWasm = opts.target.result.cpu.arch.isWasm(); - - if (isWasm) { - has_tracy = false; - } - +fn createRuntimeModule(b: *std.Build, opts: RuntimeModule.Options) RuntimeModule { const mod = b.createModule(.{ - .root_source_file = b.path("src/runtime/root.zig"), + .root_source_file = b.path("src/runtime/main.zig"), .target = opts.target, .optimize = opts.optimize, .link_libc = true @@ -139,12 +140,12 @@ fn createEngineModule(b: *std.Build, opts: EngineModule.Options) EngineModule { const dep_sokol = b.dependency("sokol", .{ .target = opts.target, .optimize = opts.optimize, - .with_sokol_imgui = has_imgui, + .with_sokol_imgui = opts.has_imgui, }); mod.linkLibrary(dep_sokol.artifact("sokol_clib")); mod.addImport("sokol", dep_sokol.module("sokol")); - if (has_imgui) { + if (opts.has_imgui) { if (b.lazyDependency("cimgui", .{ .target = opts.target, .optimize = opts.optimize, @@ -159,10 +160,10 @@ fn createEngineModule(b: *std.Build, opts: EngineModule.Options) EngineModule { const dep_tracy = b.dependency("tracy", .{ .target = opts.target, .optimize = opts.optimize, - .tracy_enable = has_tracy, + .tracy_enable = opts.has_tracy, .tracy_only_localhost = true }); - if (has_tracy) { + if (opts.has_tracy) { mod.linkLibrary(dep_tracy.artifact("tracy")); } mod.addImport("tracy", dep_tracy.module("tracy")); @@ -206,13 +207,7 @@ fn createEngineModule(b: *std.Build, opts: EngineModule.Options) EngineModule { // // TODO: Define buid config for wasm // } - 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{ + return RuntimeModule{ .root_module = mod, .dep_sokol = dep_sokol }; @@ -306,7 +301,7 @@ fn buildWasm(b: *std.Build, opts: BuildWasmOptions) *std.Build.Step { .use_webgl2 = true, .use_emmalloc = true, .use_filesystem = false, - .shell_file_path = b.path("src/shell.html"), + .shell_file_path = b.path("src/runtime/shell.html"), }) catch unreachable; return &link_step.step; diff --git a/engine/src/lib/root.zig b/engine/src/lib/root.zig index 004b107..caaeb94 100644 --- a/engine/src/lib/root.zig +++ b/engine/src/lib/root.zig @@ -1,6 +1,7 @@ const std = @import("std"); const log = std.log.scoped(.engine); +pub const build_options = @import("build_options"); pub const ImGui = @import("./imgui.zig"); pub const Math = @import("./math.zig"); @@ -285,10 +286,8 @@ pub const Frame = struct { screen_size: Vec2, clear_color: Vec4, - show_debug: bool, - - pub fn init(self: *Frame, gpa: std.mem.Allocator) void { - self.* = Frame{ + pub fn init(gpa: std.mem.Allocator) Frame { + return Frame{ .arena = std.heap.ArenaAllocator.init(gpa), .time_ns = 0, .dt_ns = 0, @@ -299,7 +298,6 @@ pub const Frame = struct { .graphics_commands = .empty, .canvas_size = null, .clear_color = rgb(0, 0, 0), - .show_debug = false, .audio_commands = .empty, .hide_cursor = false, .screen_size = .init(0, 0), @@ -471,3 +469,35 @@ pub const Init = struct { gpa: std.mem.Allocator, seed: u64 }; + +pub const Callbacks = struct { + pub const State = *anyopaque; + + pub const InitFn = *const fn(opts: *const Init) callconv(.c) State; + pub const TickFn = *const fn(state: State, frame: *Frame) callconv(.c) void; + pub const DeinitFn = *const fn(state: State) callconv(.c) void; + pub const DebugFn = *const fn(state: State, imgui: *ImGui) callconv(.c) void; + + init: InitFn, + tick: TickFn, + deinit: DeinitFn, + debug: if (build_options.has_imgui) DebugFn else void +}; + +pub fn exportCallbacksIfNeeded( + comptime init: Callbacks.InitFn, + comptime tick: Callbacks.TickFn, + comptime deinit: Callbacks.DeinitFn, + comptime debug: Callbacks.DebugFn +) void { + if (build_options.statically_linked) { + return; + } + + @export(init, .{ .name = "init", .linkage = .strong }); + @export(tick, .{ .name = "tick", .linkage = .strong }); + @export(deinit, .{ .name = "deinit", .linkage = .strong }); + if (build_options.has_imgui) { + @export(debug, .{ .name = "debug", .linkage = .strong }); + } +} diff --git a/engine/src/runtime/audio/root.zig b/engine/src/runtime/audio/root.zig index 881dc60..747ef52 100644 --- a/engine/src/runtime/audio/root.zig +++ b/engine/src/runtime/audio/root.zig @@ -13,7 +13,7 @@ pub const Mixer = @import("./mixer.zig"); pub const Command = Mixer.Command; -const Nanoseconds = @import("../root.zig").Nanoseconds; +const Nanoseconds = @import("lib").Nanoseconds; const sokol = @import("sokol"); const saudio = sokol.audio; diff --git a/engine/src/runtime/engine.zig b/engine/src/runtime/engine.zig new file mode 100644 index 0000000..ab3a237 --- /dev/null +++ b/engine/src/runtime/engine.zig @@ -0,0 +1,788 @@ +const std = @import("std"); +const log = std.log.scoped(.engine); +const assert = std.debug.assert; + +const sokol = @import("sokol"); +const sapp = sokol.app; + +pub const Input = @import("./input.zig"); + +const ScreenScalar = @import("./screen_scaler.zig"); +pub const imgui = @import("./imgui.zig"); +pub const Graphics = @import("./graphics.zig"); +pub const Audio = @import("./audio/root.zig"); +const tracy = @import("tracy"); +const builtin = @import("builtin"); +const STBImage = @import("stb_image"); + +const Gfx = Graphics; + +const Lib = @import("lib"); +const build_options = Lib.build_options; +const debug_keybinds = (builtin.mode == .Debug); +const GameCallbacks = Lib.Callbacks; +pub const Math = Lib.Math; +pub const Vec2 = Math.Vec2; +const rgb = Math.rgb; + +const GameLinking = struct { + kind: union(enum) { + static, + dynamic: struct { + fan_fd: std.os.linux.fd_t, + path: []const u8, + dir_path: [:0]const u8, + lib: std.DynLib, + last_event_at: ?std.time.Instant, + ignore_next_event: bool, + reload_index: u32 + }, + }, + callbacks: GameCallbacks, + + // TODO: Make this configurable + const event_debounce_ns = std.time.ns_per_ms * 150; + + pub fn initDynamic(gpa: std.mem.Allocator, path: []const u8) !GameLinking { + if (!build_options.hot_reload) { + return error.NotSupported; + } + + const dir_path_z = try gpa.dupeZ(u8, std.fs.path.dirname(path) orelse "."); + errdefer gpa.free(dir_path_z); + + const path_dupe = try gpa.dupe(u8, path); + errdefer gpa.free(path_dupe); + + const fan_fd: std.c.fd_t = @bitCast(@as(u32, @truncate( + std.os.linux.fanotify_init(.{ + .REPORT_FID = true, + .REPORT_DIR_FID = true, + .REPORT_NAME = true, + .NONBLOCK = true + }, @intFromEnum(std.posix.ACCMODE.RDONLY)) + ))); + if (fan_fd == -1) { + return error.fanotify_init; + } + + const mark_err = std.os.linux.fanotify_mark( + @intCast(fan_fd), + .{ .ADD = true, .ONLYDIR = true }, + .{ + .CLOSE_WRITE = true, + .CLOSE_NOWRITE = true, + .CREATE = true, + .MOVED_TO = true, + + .EVENT_ON_CHILD = true, + .ONDIR = true, + }, + @intCast(std.c.AT.FDCWD), + dir_path_z + ); + assert(mark_err == 0); + + var lib = try std.DynLib.open(path); + const callbacks = try getCallbacksFromLibrary(&lib); + + return GameLinking{ + .kind = .{ + .dynamic = .{ + .fan_fd = fan_fd, + .dir_path = dir_path_z, + .path = path_dupe, + .lib = lib, + .last_event_at = null, + .reload_index = 0, + .ignore_next_event = false + } + }, + .callbacks = callbacks + }; + } + + pub fn initStatic() !GameLinking { + if (!build_options.statically_linked) { + return error.NotStaticallyLinked; + } + + const game = @import("game"); + return GameLinking{ + .kind = .static, + .callbacks = GameCallbacks{ + .init = game.init, + .deinit = game.deinit, + .tick = game.tick, + .debug = if (build_options.has_imgui) game.debug else {}, + } + }; + } + + pub fn deinit(self: *GameLinking, gpa: std.mem.Allocator) void { + if (build_options.hot_reload and self.kind == .dynamic) { + self.kind.dynamic.lib.close(); + gpa.free(self.kind.dynamic.dir_path); + gpa.free(self.kind.dynamic.path); + std.posix.close(self.kind.dynamic.fan_fd); + } + } + + fn getCallbacksFromLibrary(lib: *std.DynLib) !GameCallbacks { + return GameCallbacks{ + .init = lib.lookup(GameCallbacks.InitFn, "init") orelse return error.MissingFunction, + .tick = lib.lookup(GameCallbacks.TickFn, "tick") orelse return error.MissingFunction, + .deinit = lib.lookup(GameCallbacks.DeinitFn, "deinit") orelse return error.MissingFunction, + .debug = if (build_options.has_imgui) lib.lookup(GameCallbacks.DebugFn, "debug") orelse return error.MissingFunction else {}, + }; + } + + pub fn check(self: *GameLinking) !void { + if (self.kind == .static) { + return; + } + + if (!build_options.hot_reload) { + return; + } + + const fanotify = std.os.linux.fanotify; + const M = fanotify.event_metadata; + const fan_fd = self.kind.dynamic.fan_fd; + + var need_to_reload = false; + + var events_buf: [256 + 4096]u8 = undefined; + while (true) { + var len = std.posix.read(fan_fd, &events_buf) catch |err| switch (err) { + error.WouldBlock => break, + else => |e| return e, + }; + var meta: [*]align(1) M = @ptrCast(&events_buf); + + while (len >= @sizeOf(M) and meta[0].event_len >= @sizeOf(M) and meta[0].event_len <= len) : ({ + len -= meta[0].event_len; + meta = @ptrCast(@as([*]u8, @ptrCast(meta)) + meta[0].event_len); + }) { + assert(meta[0].vers == M.VERSION); + if (meta[0].mask.Q_OVERFLOW) { + need_to_reload = true; + // TODO: + // std.log.warn("file system watch queue overflowed; falling back to fstat", .{}); + break; + } + + var is_lib_event = false; + const fid: *align(1) fanotify.event_info_fid = @ptrCast(meta + 1); + switch (fid.hdr.info_type) { + .DFID_NAME => { + const file_handle: *align(1) std.os.linux.file_handle = @ptrCast(&fid.handle); + const file_name_z: [*:0]u8 = @ptrCast((&file_handle.f_handle).ptr + file_handle.handle_bytes); + const file_name = std.mem.span(file_name_z); + + const lib_name = std.fs.path.basename(self.kind.dynamic.path); + is_lib_event = std.mem.eql(u8,file_name, lib_name); + }, + else => |t| log.warn("unexpected fanotify event '{s}'", .{@tagName(t)}), + } + + if (is_lib_event) { + need_to_reload = true; + } + } + } + + if (need_to_reload) { + if (self.kind.dynamic.ignore_next_event) { + self.kind.dynamic.ignore_next_event = false; + } else { + self.kind.dynamic.last_event_at = try std.time.Instant.now(); + } + } + + if (self.kind.dynamic.last_event_at) |last_event_at| { + const now = try std.time.Instant.now(); + const time_passed = now.since(last_event_at); + if (time_passed > event_debounce_ns) { + self.kind.dynamic.last_event_at = null; + + var tmp_dir = try std.fs.openDirAbsolute("/tmp", .{}); + defer tmp_dir.close(); + + var new_filename_buffer: [std.fs.max_name_bytes]u8 = undefined; + const tmp_filename = try std.fmt.bufPrint( + &new_filename_buffer, + "{s}_{}.so", + .{std.fs.path.stem(self.kind.dynamic.path), self.kind.dynamic.reload_index} + ); + + try std.fs.Dir.copyFile( + std.fs.cwd(), self.kind.dynamic.path, + tmp_dir, tmp_filename, + .{} + ); + + var tmp_path_buffer: [std.fs.max_path_bytes]u8 = undefined; + const tmp_path = try std.fmt.bufPrint( + &tmp_path_buffer, + "/tmp/{s}", + .{tmp_filename} + ); + + var new_lib = try std.DynLib.open(tmp_path); + const new_callbacks = try getCallbacksFromLibrary(&new_lib); + + try tmp_dir.deleteFile(tmp_filename); + + log.debug("Reload game code", .{}); + self.kind.dynamic.lib.close(); + self.kind.dynamic.lib = new_lib; + self.callbacks = new_callbacks; + self.kind.dynamic.reload_index += 1; + self.kind.dynamic.ignore_next_event = true; + } + } + } +}; + +const GameState = struct { + state: GameCallbacks.State, + + input: Input, + frame: Lib.Frame, + started_at: std.time.Instant, + last_frame_at: Lib.Nanoseconds, + previous_tick_canvas_size: ?Vec2, + seed: u64, + + const Options = struct { + gpa: std.mem.Allocator, + seed: u64 + }; + + pub fn init(opts: Options) GameState { + return GameState{ + .input = .initial, + .frame = .init(opts.gpa), + .started_at = std.time.Instant.now() catch @panic("Instant.now() unsupported"), + .last_frame_at = 0, + .previous_tick_canvas_size = null, + .seed = opts.seed, + .state = undefined + }; + } + + pub fn deinit(self: *GameState) void { + self.frame.deinit(); + } +}; + +const Engine = @This(); + +allocator: std.mem.Allocator, +graphics: Graphics, + +game_linking: GameLinking, +game_state: GameState, + +seed: u64, + +show_debug: bool, +restart_game: bool, + +const RunOptions = struct { + allocator: std.mem.Allocator, + game_library_path: ?[]const u8 = null +}; + +pub fn run(self: *Engine, opts: RunOptions) !void { + self.* = Engine{ + .allocator = opts.allocator, + .graphics = undefined, + .game_state = undefined, + .game_linking = undefined, + .show_debug = false, + .restart_game = false, + .seed = @bitCast(std.time.milliTimestamp()), + }; + + if (opts.game_library_path) |game_library_path| { + self.game_linking = try .initDynamic(self.allocator, game_library_path); + } else { + self.game_linking = try .initStatic(); + } + + tracy.setThreadName("Main"); + + if (builtin.os.tag == .linux) { + var sa: std.posix.Sigaction = .{ + .handler = .{ .handler = posixSignalHandler }, + .mask = std.posix.sigemptyset(), + .flags = std.posix.SA.RESTART, + }; + std.posix.sigaction(std.posix.SIG.INT, &sa, null); + } + + log.debug("Build options:", .{}); + inline for (@typeInfo(build_options).@"struct".decls) |decl| { + log.debug("- {s}: {}", .{decl.name, @field(build_options, decl.name)}); + } + + // TODO: Don't hard code icon path, allow changing through options + // var icon_data = try STBImage.load(@embedFile("../assets/icon.png")); + // defer icon_data.deinit(); + + var icon: sapp.IconDesc = .{}; + icon.sokol_default = true; + // TODO: + // icon.images[0] = .{ + // .width = @intCast(icon_data.width), + // .height = @intCast(icon_data.height), + // .pixels = .{ + // .ptr = icon_data.rgba8_pixels, + // .size = icon_data.width * icon_data.height * 4 + // } + // }; + + sapp.run(.{ + .init_userdata_cb = sokolInitCallback, + .frame_userdata_cb = sokolFrameCallback, + .cleanup_userdata_cb = sokolCleanupCallback, + .event_userdata_cb = sokolEventCallback, + .user_data = self, + .width = 640, + .height = 480, + .icon = icon, + .window_title = "Game", + .logger = .{ .func = sokolLogCallback }, + .win32 = .{ + .console_utf8 = true + } + }); +} + +fn sokolInit(self: *Engine) !void { + const zone = tracy.initZone(@src(), .{ }); + defer zone.deinit(); + + try self.graphics.init(.{ + .allocator = self.allocator, + .logger = .{ .func = sokolLogCallback }, + // TODO: + // .imgui_font = .{ + // .ttf_data = @embedFile("../assets/roboto-font/Roboto-Regular.ttf"), + // } + }); + + try Audio.init(.{ + .allocator = self.allocator, + .logger = .{ .func = sokolLogCallback }, + }); + + self.game_state = .init(.{ + .gpa = self.allocator, + .seed = self.seed + }); + + const opts = Lib.Init{ + .gpa = self.allocator, + .seed = self.game_state.seed, + }; + self.game_state.state = self.game_linking.callbacks.init(&opts); +} + +fn sokolCleanup(self: *Engine) void { + const zone = tracy.initZone(@src(), .{ }); + defer zone.deinit(); + + self.game_linking.callbacks.deinit(self.game_state.state); + self.game_state.deinit(); + Audio.deinit(); + self.graphics.deinit(self.allocator); + self.game_linking.deinit(self.allocator); +} + +fn sokolFrame(self: *Engine) !void { + tracy.frameMark(); + + const zone = tracy.initZone(@src(), .{ }); + defer zone.deinit(); + + const frame = &self.game_state.frame; + + try self.game_linking.check(); + + const screen_size = Vec2.init(sapp.widthf(), sapp.heightf()); + + var maybe_screen_scaler: ?ScreenScalar = null; + if (self.game_state.previous_tick_canvas_size) |canvas_size| { + maybe_screen_scaler = ScreenScalar.init(screen_size, canvas_size); + } + + { + const now = std.time.Instant.now() catch @panic("Instant.now() unsupported"); + const time_passed = now.since(self.game_state.started_at); + defer self.game_state.last_frame_at = time_passed; + + _ = frame.arena.reset(.retain_capacity); + const arena = frame.arena.allocator(); + + const audio_commands_capacity = frame.audio_commands.capacity; + frame.audio_commands = .empty; + try frame.audio_commands.ensureTotalCapacity(arena, audio_commands_capacity); + + const graphics_commands_capacity = frame.graphics_commands.capacity; + frame.graphics_commands = .empty; + try frame.graphics_commands.ensureTotalCapacity(arena, graphics_commands_capacity); + + frame.screen_size = screen_size; + frame.time_ns = time_passed; + frame.dt_ns = time_passed - self.game_state.last_frame_at; + + frame.mouse_position = self.game_state.input.mouse_position; + if (maybe_screen_scaler) |screen_scaler| { + screen_scaler.push(frame); + + if (frame.mouse_position) |mouse_position| { + frame.mouse_position = mouse_position.sub(screen_scaler.translation).divideScalar(screen_scaler.scale); + } + } + + if (debug_keybinds) { + if (frame.isKeyPressed(.F3)) { + self.show_debug = !self.show_debug; + } + + if (frame.isKeyPressed(.F5)) { + self.restart_game = true; + } + } + + self.game_linking.callbacks.tick(self.game_state.state, frame); + + if (maybe_screen_scaler) |screen_scaler| { + screen_scaler.pop(frame, frame.clear_color); + } + + frame.keyboard.pressed = .initEmpty(); + frame.keyboard.released = .initEmpty(); + frame.mouse_button.pressed = .initEmpty(); + frame.mouse_button.released = .initEmpty(); + } + + // Canvas size modification must always be applied a frame later. + // So that mouse coordinate transformations are consistent. + self.game_state.previous_tick_canvas_size = frame.canvas_size; + + sapp.showMouse(!frame.hide_cursor); + + { + self.graphics.beginFrame(); + defer self.graphics.endFrame(frame.clear_color); + + self.graphics.drawCommands(frame.graphics_commands.items); + + if (self.show_debug and build_options.has_imgui) { + try self.showDebugWindow(frame); + } + } + + for (frame.audio_commands.items) |command| { + try Audio.mixer.commands.push(command); + } + + if (self.restart_game) { + self.restart_game = false; + + self.game_linking.callbacks.deinit(self.game_state.state); + + self.game_state.deinit(); + self.game_state = .init(.{ + .gpa = self.allocator, + .seed = self.seed + }); + + const opts = Lib.Init{ + .gpa = self.allocator, + .seed = self.game_state.seed, + }; + self.game_state.state = self.game_linking.callbacks.init(&opts); + } +} + +fn showDebugWindow(self: *Engine, frame: *Lib.Frame) !void { + if (!imgui.beginWindow(.{ + .name = "Debug", + .pos = Vec2.init(20, 20), + .size = Vec2.init(200, 200), + })) { + return; + } + defer imgui.endWindow(); + + _ = imgui.beginTabBar("debug"); + defer imgui.endTabBar(); + + if (imgui.beginTabItem("Game")) { + defer imgui.endTabItem(); + + var imgui_ctx = Lib.ImGui.init(self.allocator); + defer imgui_ctx.deinit(); + self.game_linking.callbacks.debug(self.game_state.state, &imgui_ctx); + for (imgui_ctx.commands.items) |cmd| { + switch (cmd) { + .text => |str| imgui.text(str) + } + } + } + + if (imgui.beginTabItem("Engine")) { + defer imgui.endTabItem(); + + if (imgui.button("Restart")) { + self.restart_game = true; + } + + const linking_label = if (self.game_linking.kind == .static) "static" else "dynamic"; + imgui.textFmt("Linking: {s}", .{ linking_label }); + + imgui.textFmt("Seed: 0x{x:08}", .{ self.seed }); + + const time_ms: f64 = @floatFromInt(@divFloor(self.game_state.last_frame_at, std.time.ns_per_ms)); + imgui.textFmt("Time: {:.2}", .{ time_ms / 1000 }); + + imgui.textFmt("Draw commands: {}\n", .{ + frame.graphics_commands.items.len, + }); + 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 { + const zone = tracy.initZone(@src(), .{ }); + defer zone.deinit(); + + const e = e_ptr.*; + + const input = &self.game_state.input; + const frame = &self.game_state.frame; + + if (imgui.handleEvent(e)) { + if (input.mouse_position != null) { + input.processEvent(frame, .{ + .mouse_leave = {} + }); + } + input.mouse_position = null; + return true; + } + + blk: switch (e.type) { + .MOUSE_DOWN => { + const mouse_button = Input.getMouseButtonFromSokol(e.mouse_button) orelse break :blk; + + input.processEvent(frame, .{ + .mouse_pressed = .{ + .button = mouse_button, + .position = Vec2.init(e.mouse_x, e.mouse_y) + } + }); + + return true; + }, + .MOUSE_UP => { + const mouse_button = Input.getMouseButtonFromSokol(e.mouse_button) orelse break :blk; + + input.processEvent(frame, .{ + .mouse_released = .{ + .button = mouse_button, + .position = Vec2.init(e.mouse_x, e.mouse_y) + } + }); + + return true; + }, + .MOUSE_MOVE => { + if (input.mouse_position == null) { + input.processEvent(frame, .{ + .mouse_enter = Vec2.init(e.mouse_x, e.mouse_y) + }); + } else { + input.processEvent(frame, .{ + .mouse_move = Vec2.init(e.mouse_x, e.mouse_y) + }); + } + + return true; + }, + .MOUSE_ENTER => { + if (input.mouse_position == null) { + input.processEvent(frame, .{ + .mouse_enter = Vec2.init(e.mouse_x, e.mouse_y) + }); + } + return true; + }, + .RESIZED => { + if (input.mouse_position != null) { + input.processEvent(frame, .{ + .mouse_leave = {} + }); + } + + input.processEvent(frame, .{ + .window_resize = {} + }); + return true; + }, + .MOUSE_LEAVE => { + if (input.mouse_position != null) { + input.processEvent(frame, .{ + .mouse_leave = {} + }); + } + return true; + }, + .MOUSE_SCROLL => { + input.processEvent(frame, .{ + .mouse_scroll = Vec2.init(e.scroll_x, e.scroll_y) + }); + + return true; + }, + .KEY_DOWN => { + const key_code = Input.getKeyCodeFromSokol(e.key_code) orelse break :blk; + + input.processEvent(frame, .{ + .key_pressed = .{ + .code = key_code, + .repeat = e.key_repeat + } + }); + + return true; + }, + .KEY_UP => { + const key_code = Input.getKeyCodeFromSokol(e.key_code) orelse break :blk; + + input.processEvent(frame, .{ + .key_released = key_code + }); + + return true; + }, + .CHAR => { + input.processEvent(frame, .{ + .char = @intCast(e.char_code) + }); + + return true; + }, + .QUIT_REQUESTED => { + // TODO: handle quit request. Maybe show confirmation window in certain cases. + }, + else => {} + } + + return false; +} + +fn sokolEventCallback(e_ptr: [*c]const sapp.Event, userdata: ?*anyopaque) callconv(.c) void { + const engine: *Engine = @alignCast(@ptrCast(userdata)); + + const consume_event = engine.sokolEvent(e_ptr) catch |e| blk: { + log.err("sokolEvent() failed: {}", .{e}); + if (@errorReturnTrace()) |trace| { + std.debug.dumpStackTrace(trace.*); + } + break :blk false; + }; + + if (consume_event) { + sapp.consumeEvent(); + } +} + +fn sokolCleanupCallback(userdata: ?*anyopaque) callconv(.c) void { + const engine: *Engine = @alignCast(@ptrCast(userdata)); + + engine.sokolCleanup(); +} + +fn sokolInitCallback(userdata: ?*anyopaque) callconv(.c) void { + const engine: *Engine = @alignCast(@ptrCast(userdata)); + + engine.sokolInit() catch |e| { + log.err("sokolInit() failed: {}", .{e}); + if (@errorReturnTrace()) |trace| { + std.debug.dumpStackTrace(trace.*); + } + sapp.requestQuit(); + }; +} + +fn sokolFrameCallback(userdata: ?*anyopaque) callconv(.c) void { + const engine: *Engine = @alignCast(@ptrCast(userdata)); + + engine.sokolFrame() catch |e| { + log.err("sokolFrame() failed: {}", .{e}); + if (@errorReturnTrace()) |trace| { + std.debug.dumpStackTrace(trace.*); + } + sapp.requestQuit(); + }; +} + +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 cStrToZig(c_str: [*c]const u8) [:0]const u8 { + return std.mem.span(c_str); +} + +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) { + sokolLogFmt( + log_level, + "[{s}][id:{}] {s}:{}: {s}", + .{ + cStrToZig(tag orelse "-"), + log_item, + std.fs.path.basename(cStrToZig(filename orelse "-")), + line_nr, + cStrToZig(message orelse "") + } + ); + } else { + sokolLogFmt( + log_level, + "[{s}][id:{}] {s}", + .{ + cStrToZig(tag orelse "-"), + log_item, + cStrToZig(message orelse "") + } + ); + } +} + +fn posixSignalHandler(sig: i32) callconv(.c) void { + _ = sig; + sapp.requestQuit(); +} diff --git a/engine/src/runtime/imgui.zig b/engine/src/runtime/imgui.zig index aa63c40..42adb31 100644 --- a/engine/src/runtime/imgui.zig +++ b/engine/src/runtime/imgui.zig @@ -1,7 +1,5 @@ const std = @import("std"); -const Lib = @import("lib"); const Math = Lib.Math; -const build_options = @import("build_options"); pub const ig = @import("cimgui"); const Vec2 = Math.Vec2; const Vec3 = Math.Vec3; @@ -11,6 +9,8 @@ const sokol = @import("sokol"); const sapp = sokol.app; const simgui = sokol.imgui; +const Lib = @import("lib"); +const build_options = Lib.build_options; const enabled = build_options.has_imgui; var global_allocator: ?std.mem.Allocator = null; diff --git a/engine/src/runtime/input.zig b/engine/src/runtime/input.zig index 1907c41..6489008 100644 --- a/engine/src/runtime/input.zig +++ b/engine/src/runtime/input.zig @@ -9,8 +9,6 @@ const Vec2 = lib.Math.Vec2; const Input = @This(); -const SokolKeyCodeInfo = @typeInfo(sokol.app.Keycode); - pub const Event = union(enum) { mouse_pressed: struct { button: lib.MouseButton, @@ -33,7 +31,11 @@ pub const Event = union(enum) { char: u21, }; -mouse_position: ?Vec2 = null, +mouse_position: ?Vec2, + +pub const initial = Input{ + .mouse_position = null +}; pub fn processEvent(self: *Input, frame: *Frame, event: Event) void { switch (event) { diff --git a/engine/src/runtime/main.zig b/engine/src/runtime/main.zig index 58a6b94..99a5876 100644 --- a/engine/src/runtime/main.zig +++ b/engine/src/runtime/main.zig @@ -1,6 +1,6 @@ const std = @import("std"); const builtin = @import("builtin"); -const Engine = @import("engine"); +const Engine = @import("./engine.zig"); var engine: Engine = undefined; diff --git a/engine/src/runtime/root.zig b/engine/src/runtime/root.zig deleted file mode 100644 index 77aa877..0000000 --- a/engine/src/runtime/root.zig +++ /dev/null @@ -1,539 +0,0 @@ -const std = @import("std"); -const log = std.log.scoped(.engine); -const assert = std.debug.assert; - -const sokol = @import("sokol"); -const sapp = sokol.app; - -pub const Input = @import("./input.zig"); - -const ScreenScalar = @import("./screen_scaler.zig"); -pub const imgui = @import("./imgui.zig"); -pub const Graphics = @import("./graphics.zig"); -pub const Audio = @import("./audio/root.zig"); -const tracy = @import("tracy"); -const builtin = @import("builtin"); -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 { - 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 -}; - -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; - } - - const game = @import("game"); - return GameCallbacks{ - .init = game.init, - .deinit = game.deinit, - .tick = game.tick, - .debug = if (build_options.has_imgui) game.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(); - -allocator: std.mem.Allocator, -started_at: std.time.Instant, -last_frame_at: Lib.Nanoseconds, -graphics: Graphics, -input: Input, - -game_linking: GameLinking, -game_callbacks: GameCallbacks, - -frame: Lib.Frame, - -previous_tick_canvas_size: ?Vec2 = null, - -const RunOptions = struct { - allocator: std.mem.Allocator, - game_library_path: ?[]const u8 = null -}; - -pub fn run(self: *Engine, opts: RunOptions) !void { - self.* = Engine{ - .allocator = opts.allocator, - .started_at = std.time.Instant.now() catch @panic("Instant.now() unsupported"), - .last_frame_at = 0, - .frame = undefined, - .graphics = undefined, - .game_linking = undefined, - .game_callbacks = undefined, - .input = .{} - }; - - if (opts.game_library_path) |game_library_path| { - self.game_linking = .{ - .dynamic = .{ - .lib = try std.DynLib.open(game_library_path) - } - }; - } else { - self.game_linking = .static; - } - - self.game_callbacks = try self.game_linking.getCallbacks(); - - self.frame.init(self.allocator); - - tracy.setThreadName("Main"); - - if (builtin.os.tag == .linux) { - var sa: std.posix.Sigaction = .{ - .handler = .{ .handler = posixSignalHandler }, - .mask = std.posix.sigemptyset(), - .flags = std.posix.SA.RESTART, - }; - std.posix.sigaction(std.posix.SIG.INT, &sa, null); - } - - // TODO: Don't hard code icon path, allow changing through options - // var icon_data = try STBImage.load(@embedFile("../assets/icon.png")); - // defer icon_data.deinit(); - - var icon: sapp.IconDesc = .{}; - icon.sokol_default = true; - // TODO: - // icon.images[0] = .{ - // .width = @intCast(icon_data.width), - // .height = @intCast(icon_data.height), - // .pixels = .{ - // .ptr = icon_data.rgba8_pixels, - // .size = icon_data.width * icon_data.height * 4 - // } - // }; - - sapp.run(.{ - .init_userdata_cb = sokolInitCallback, - .frame_userdata_cb = sokolFrameCallback, - .cleanup_userdata_cb = sokolCleanupCallback, - .event_userdata_cb = sokolEventCallback, - .user_data = self, - .width = 640, - .height = 480, - .icon = icon, - .window_title = "Game", - .logger = .{ .func = sokolLogCallback }, - .win32 = .{ - .console_utf8 = true - } - }); -} - -fn sokolInit(self: *Engine) !void { - const zone = tracy.initZone(@src(), .{ }); - defer zone.deinit(); - - log.debug("Init", .{}); - - try self.graphics.init(.{ - .allocator = self.allocator, - .logger = .{ .func = sokolLogCallback }, - // TODO: - // .imgui_font = .{ - // .ttf_data = @embedFile("../assets/roboto-font/Roboto-Regular.ttf"), - // } - }); - - try Audio.init(.{ - .allocator = self.allocator, - .logger = .{ .func = sokolLogCallback }, - }); - - const seed: u64 = @bitCast(std.time.milliTimestamp()); - - const opts = Lib.Init{ - .gpa = self.allocator, - .seed = seed, - }; - self.game_callbacks.init(&opts); -} - -fn sokolCleanup(self: *Engine) void { - const zone = tracy.initZone(@src(), .{ }); - defer zone.deinit(); - - self.frame.deinit(); - Audio.deinit(); - self.game_callbacks.deinit(); - self.graphics.deinit(self.allocator); -} - -fn sokolFrame(self: *Engine) !void { - tracy.frameMark(); - - const zone = tracy.initZone(@src(), .{ }); - defer zone.deinit(); - - 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 screen_size = Vec2.init(sapp.widthf(), sapp.heightf()); - - var maybe_screen_scaler: ?ScreenScalar = null; - if (self.previous_tick_canvas_size) |canvas_size| { - maybe_screen_scaler = ScreenScalar.init(screen_size, canvas_size); - } - - { - _ = frame.arena.reset(.retain_capacity); - const arena = frame.arena.allocator(); - - const audio_commands_capacity = frame.audio_commands.capacity; - frame.audio_commands = .empty; - try frame.audio_commands.ensureTotalCapacity(arena, audio_commands_capacity); - - const graphics_commands_capacity = frame.graphics_commands.capacity; - frame.graphics_commands = .empty; - try frame.graphics_commands.ensureTotalCapacity(arena, graphics_commands_capacity); - - frame.screen_size = screen_size; - frame.time_ns = time_passed; - frame.dt_ns = time_passed - self.last_frame_at; - - frame.mouse_position = self.input.mouse_position; - if (maybe_screen_scaler) |screen_scaler| { - screen_scaler.push(frame); - - if (frame.mouse_position) |mouse_position| { - frame.mouse_position = mouse_position.sub(screen_scaler.translation).divideScalar(screen_scaler.scale); - } - } - - self.game_callbacks.tick(frame); - - if (maybe_screen_scaler) |screen_scaler| { - screen_scaler.pop(frame, frame.clear_color); - } - - frame.keyboard.pressed = .initEmpty(); - frame.keyboard.released = .initEmpty(); - frame.mouse_button.pressed = .initEmpty(); - frame.mouse_button.released = .initEmpty(); - } - - // Canvas size modification must always be applied a frame later. - // So that mouse coordinate transformations are consistent. - self.previous_tick_canvas_size = self.frame.canvas_size; - - sapp.showMouse(!self.frame.hide_cursor); - - { - self.graphics.beginFrame(); - defer self.graphics.endFrame(frame.clear_color); - - self.graphics.drawCommands(frame.graphics_commands.items); - - if (frame.show_debug and build_options.has_imgui) { - try self.showDebugWindow(frame); - } - } - - for (frame.audio_commands.items) |command| { - try Audio.mixer.commands.push(command); - } -} - -fn showDebugWindow(self: *Engine, frame: *Lib.Frame) !void { - if (!imgui.beginWindow(.{ - .name = "Debug", - .pos = Vec2.init(20, 20), - .size = Vec2.init(200, 200), - })) { - return; - } - defer imgui.endWindow(); - - _ = imgui.beginTabBar("debug"); - defer imgui.endTabBar(); - - if (imgui.beginTabItem("Game")) { - defer imgui.endTabItem(); - - var imgui_ctx = Lib.ImGui.init(self.allocator); - defer imgui_ctx.deinit(); - self.game_callbacks.debug(&imgui_ctx); - for (imgui_ctx.commands.items) |cmd| { - switch (cmd) { - .text => |str| imgui.text(str) - } - } - } - - if (imgui.beginTabItem("Engine")) { - defer imgui.endTabItem(); - imgui.textFmt("Draw commands: {}\n", .{ - frame.graphics_commands.items.len, - }); - 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 { - const zone = tracy.initZone(@src(), .{ }); - defer zone.deinit(); - - const e = e_ptr.*; - - if (imgui.handleEvent(e)) { - if (self.input.mouse_position != null) { - self.input.processEvent(&self.frame, .{ - .mouse_leave = {} - }); - } - self.input.mouse_position = null; - return true; - } - - blk: switch (e.type) { - .MOUSE_DOWN => { - const mouse_button = Input.getMouseButtonFromSokol(e.mouse_button) orelse break :blk; - - self.input.processEvent(&self.frame, .{ - .mouse_pressed = .{ - .button = mouse_button, - .position = Vec2.init(e.mouse_x, e.mouse_y) - } - }); - - return true; - }, - .MOUSE_UP => { - const mouse_button = Input.getMouseButtonFromSokol(e.mouse_button) orelse break :blk; - - self.input.processEvent(&self.frame, .{ - .mouse_released = .{ - .button = mouse_button, - .position = Vec2.init(e.mouse_x, e.mouse_y) - } - }); - - return true; - }, - .MOUSE_MOVE => { - if (self.input.mouse_position == null) { - self.input.processEvent(&self.frame, .{ - .mouse_enter = Vec2.init(e.mouse_x, e.mouse_y) - }); - } else { - self.input.processEvent(&self.frame, .{ - .mouse_move = Vec2.init(e.mouse_x, e.mouse_y) - }); - } - - return true; - }, - .MOUSE_ENTER => { - if (self.input.mouse_position == null) { - self.input.processEvent(&self.frame, .{ - .mouse_enter = Vec2.init(e.mouse_x, e.mouse_y) - }); - } - return true; - }, - .RESIZED => { - if (self.input.mouse_position != null) { - self.input.processEvent(&self.frame, .{ - .mouse_leave = {} - }); - } - - self.input.processEvent(&self.frame, .{ - .window_resize = {} - }); - return true; - }, - .MOUSE_LEAVE => { - if (self.input.mouse_position != null) { - self.input.processEvent(&self.frame, .{ - .mouse_leave = {} - }); - } - return true; - }, - .MOUSE_SCROLL => { - self.input.processEvent(&self.frame, .{ - .mouse_scroll = Vec2.init(e.scroll_x, e.scroll_y) - }); - - return true; - }, - .KEY_DOWN => { - const key_code = Input.getKeyCodeFromSokol(e.key_code) orelse break :blk; - - self.input.processEvent(&self.frame, .{ - .key_pressed = .{ - .code = key_code, - .repeat = e.key_repeat - } - }); - - return true; - }, - .KEY_UP => { - const key_code = Input.getKeyCodeFromSokol(e.key_code) orelse break :blk; - - self.input.processEvent(&self.frame, .{ - .key_released = key_code - }); - - return true; - }, - .CHAR => { - self.input.processEvent(&self.frame, .{ - .char = @intCast(e.char_code) - }); - - return true; - }, - .QUIT_REQUESTED => { - // TODO: handle quit request. Maybe show confirmation window in certain cases. - }, - else => {} - } - - return false; -} - -fn sokolEventCallback(e_ptr: [*c]const sapp.Event, userdata: ?*anyopaque) callconv(.c) void { - const engine: *Engine = @alignCast(@ptrCast(userdata)); - - const consume_event = engine.sokolEvent(e_ptr) catch |e| blk: { - log.err("sokolEvent() failed: {}", .{e}); - if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); - } - break :blk false; - }; - - if (consume_event) { - sapp.consumeEvent(); - } -} - -fn sokolCleanupCallback(userdata: ?*anyopaque) callconv(.c) void { - const engine: *Engine = @alignCast(@ptrCast(userdata)); - - engine.sokolCleanup(); -} - -fn sokolInitCallback(userdata: ?*anyopaque) callconv(.c) void { - const engine: *Engine = @alignCast(@ptrCast(userdata)); - - engine.sokolInit() catch |e| { - log.err("sokolInit() failed: {}", .{e}); - if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); - } - sapp.requestQuit(); - }; -} - -fn sokolFrameCallback(userdata: ?*anyopaque) callconv(.c) void { - const engine: *Engine = @alignCast(@ptrCast(userdata)); - - engine.sokolFrame() catch |e| { - log.err("sokolFrame() failed: {}", .{e}); - if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); - } - sapp.requestQuit(); - }; -} - -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 cStrToZig(c_str: [*c]const u8) [:0]const u8 { - return std.mem.span(c_str); -} - -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) { - sokolLogFmt( - log_level, - "[{s}][id:{}] {s}:{}: {s}", - .{ - cStrToZig(tag orelse "-"), - log_item, - std.fs.path.basename(cStrToZig(filename orelse "-")), - line_nr, - cStrToZig(message orelse "") - } - ); - } else { - sokolLogFmt( - log_level, - "[{s}][id:{}] {s}", - .{ - cStrToZig(tag orelse "-"), - log_item, - cStrToZig(message orelse "") - } - ); - } -} - -fn posixSignalHandler(sig: i32) callconv(.c) void { - _ = sig; - sapp.requestQuit(); -} diff --git a/src/game.zig b/src/game.zig index 851bf62..aba0765 100644 --- a/src/game.zig +++ b/src/game.zig @@ -25,9 +25,7 @@ const State = struct { // wood01: Audio.Data.Id, }; -var g_state: *State = undefined; - -pub export fn init(opts: *const Engine.Init) void { +pub fn init(opts: *const Engine.Init) callconv(.c) *anyopaque { const gpa = opts.gpa; // const font_id_array: FontName.EnumArray = .init(.{ @@ -41,32 +39,31 @@ pub export fn init(opts: *const Engine.Init) void { // .data = @embedFile("assets/wood01.ogg"), // }); - g_state = gpa.create(State) catch @panic("OOM"); - g_state.* = State{ + const state = gpa.create(State) catch @panic("OOM"); + state.* = State{ .gpa = gpa, .player = .init(50, 50), // .font_id = font_id_array, // .wood01 = wood01 }; + + return state; } -pub export fn deinit() void { - const gpa = g_state.gpa; +pub fn deinit(state_ptr: *anyopaque) callconv(.c) void { + const state: *State = @alignCast(@ptrCast(state_ptr)); + const gpa = state.gpa; - gpa.destroy(g_state); - g_state = undefined; + gpa.destroy(state); } -pub export fn tick(frame: *Engine.Frame) void { +pub fn tick(state_ptr: *anyopaque, frame: *Engine.Frame) callconv(.c) void { + const state: *State = @alignCast(@ptrCast(state_ptr)); const dt = frame.deltaTime(); const canvas_size = Vec2.init(100, 100); frame.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; @@ -89,7 +86,7 @@ pub export fn tick(frame: *Engine.Frame) void { // }); } - g_state.player = g_state.player.add(dir.multiplyScalar(50 * dt)); + state.player = state.player.add(dir.multiplyScalar(50 * dt)); frame.drawRectangle(.{ .rect = .{ .pos = .init(0, 0), .size = canvas_size }, @@ -98,13 +95,13 @@ pub export fn tick(frame: *Engine.Frame) void { const size = Vec2.init(20, 20); frame.drawRectangle(.{ .rect = .{ - .pos = g_state.player.sub(size.divideScalar(2)), + .pos = state.player.sub(size.divideScalar(2)), .size = size }, - .color = rgb(200, 20, 20) + .color = rgb(200, 2, 200) }); if (dir.x != 0 or dir.y != 0) { - frame.drawRectanglOutline(g_state.player.sub(size.divideScalar(2)), size, rgb(20, 200, 20), 3); + frame.drawRectanglOutline(state.player.sub(size.divideScalar(2)), size, rgb(20, 20, 20), 3); } // const regular_font = g_state.font_id.get(.regular); @@ -114,6 +111,12 @@ pub export fn tick(frame: *Engine.Frame) void { // }); } -pub export fn debug(imgui: *Engine.ImGui) void { +pub fn debug(state_ptr: *anyopaque, imgui: *Engine.ImGui) callconv(.c) void { + const state: *State = @alignCast(@ptrCast(state_ptr)); + _ = state; // autofix imgui.text("Hello World!\n", .{}); } + +comptime { + Engine.exportCallbacksIfNeeded(init, tick, deinit, debug); +}