adding file reloading

This commit is contained in:
Peter Li 2025-05-17 14:45:23 -07:00
parent 63fe7961b1
commit 1ee804355c
20 changed files with 1590 additions and 7 deletions

View File

View File

@ -248,5 +248,7 @@ pub const console = @import("console.zig");
pub const configVars = @import("configVars.zig");
pub const loopDelay = engine.loopDelay;
pub const getConfigVar = configVars.getConfigVar;
pub const configVar = configVars.configVar;

View File

@ -388,4 +388,20 @@ pub const NeonObjectParams = struct {
isCore: bool = false,
};
const Src = std.builtin.SourceLocation;
pub fn loopDelay(comptime src: Src, interval: f64, dt: f64, comptime S: type, capture: S) void {
const C = struct {
pub const s = src;
pub var __timeleft: f64 = 0.0;
pub var __interval: f64 = 0.0;
};
C.__interval -= dt;
if (C.__timeleft <= 0.0) {
S.func(capture);
while (C.__timeleft < 0)
C.__timeleft += interval;
}
}
test "comptime registration implementation" {}

64
engine/core/src/extern/externModule.zig vendored Normal file
View File

@ -0,0 +1,64 @@
// modules for managing loading dynamic libraries
//
// i want hot swapping
//
// 1. add a folder to the watch list
pub const FileWatchEntry = struct {
path: []const u8,
stamp: i128 = 0,
cbCtx: ?*anyopaque,
loadCallback: *const fn (*@This(), ?*anyopaque) void,
// staging path is under .cache/modules/<module hash>/
pub fn copyToStaging(self: *@This()) !void {
const fileName = p2.
const dir = try std.fs.cwd().makePath();
std.fs.cwd().copyFile(self.path, dir, "", options: CopyFileOptions)
}
pub fn checkUpdateTime(self: *@This()) bool {
const stat = std.fs.cwd().statFile(self.path) catch false;
if (stat.mtime != self.stamp) {
self.stamp = stat.mtime;
}
}
};
pub const ModuleLoader = struct {
backingAllocator: std.mem.Allocator,
arena: std.heap.ArenaAllocator,
allocator: std.mem.Allocator = undefined,
files: std.ArrayListUnmanaged(FileWatchEntry) = .{},
pub fn create(allocator: std.mem.Allocator) !*@This() {
const self = try allocator.create(@This());
self.* = .{
.backingAllocator = allocator,
.arena = try std.heap.ArenaAllocator.init(allocator),
};
self.allocator = self.arena.allocator();
return self;
}
pub fn destroy(self: *@This()) void {
self.arena.deinit();
self.backingAllocator.destroy(self);
}
pub fn checkForFileUpdates(self: *@This()) void {
for (self.files.items) |*f| {
if (f.checkTime()) {
f.loadCallback(f.cbCtx);
}
}
}
};
const std = @import("std");
const p2 = @import("p2");

View File

@ -0,0 +1,11 @@
// script objects.
pub const ObjectTable = extern struct {
create: *const fn (allocator: *anyopaque) callconv(.C) ?*anyopaque,
tick: *const fn (*anyopaque, f64) callconv(.C) void,
destroy: *const fn (p: *anyopaque) callconv(.C) ?*anyopaque,
};
// eg. in C
//
// object Struc

View File

@ -0,0 +1 @@

View File

@ -25,7 +25,7 @@ test "simple systems setup for core" {
try test_consoleCommands();
try test_gameObjects(std.testing.allocator);
try generateRandomSamples();
// try generateRandomSamples();
try memory.dumpTimeline("test-core-timeline.txt");
}

View File

@ -0,0 +1,15 @@
allocator: std.mem.allocator,
pub fn create(allocator: std.mem.Allocator) !*@This() {
const self = try allocator.create(@This());
self.* = .{
.allocator = allocator,
};
return self;
}
const std = @import("std");
const backlog = @import("Backlog");
const core = backlog.core;

View File

@ -0,0 +1,17 @@
allocator: std.mem.allocator,
pub fn create(allocator: std.mem.Allocator) !*@This() {
const self = try allocator.create(@This());
self.* = .{
.allocator = allocator,
};
return self;
}
const std = @import("std");
const backlog = @import("Backlog");
const core = backlog.core;
const rend = backlog.rend;
const ig = backlog.imgui.api;

View File

@ -4627,6 +4627,11 @@ pub inline fn textFmt(comptime fmt: []const u8, args: anytype) !void {
textUnformatted(@ptrCast(buffer.ptr), buffer.ptr + buffer.len);
}
pub inline fn textf(comptime f: []const u8, args: anytype) void {
const buffer = std.fmt.bufPrint(&textFmtBuffer, f, args) catch return;
textUnformatted(@ptrCast(buffer.ptr), buffer.ptr + buffer.len);
}
// pub inline fn textV(fmt: [*c]const u8, args: list) void { //igTextV
// c.igTextV(fmt, args);
// }

10
lib/p2/src/p2.zig vendored
View File

@ -73,6 +73,16 @@ pub const Span = spans.Span;
pub const shell = @import("utils/shell.zig");
pub fn sharedLibName(comptime s: []const u8) []const u8 {
const os_tag = @import("builtin").os.tag;
if (os_tag == .linux) {
return s ++ ".so";
} else if (os_tag == .macos) {
return s ++ ".dynlib";
} else {
return s ++ ".dll";
}
}
comptime {
std.testing.refAllDecls(utils);
std.testing.refAllDecls(static_structures);

View File

@ -10,8 +10,13 @@ pub fn build(b: *std.Build) void {
.target = target,
.optimize = optimize,
.root_source_file = b.path("src/packer.zig"),
.link_libc = true,
.link_libcpp = true,
});
mod.addIncludePath(b.path("src/include"));
mod.addCSourceFile(.{ .file = b.path("src/watcher.cpp"), .flags = &.{} });
const p2mod = p2dep.module("p2");
mod.addImport("p2", p2mod);

1246
lib/packer/src/include/FileWatch.hpp vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -53,6 +53,16 @@ pub const Settings = struct {
contentFolderExtraPaths: []const []const u8 = &[_][]const u8{}, // By default, this will mount the content/ folder on disk.
};
pub const WatchCallback = struct {
func: *const fn ([]const u8, ?*anyopaque) void,
ctx: ?*anyopaque = null,
path: []const u8,
pub inline fn call(self: *@This(), path: []const u8) void {
self.func(path, self.ctx);
}
};
pub const PackerFS = struct {
allocator: std.mem.Allocator,
fileHeaders: std.ArrayListUnmanaged(PackedFileEntry) = .{},
@ -67,6 +77,8 @@ pub const PackerFS = struct {
lock: std.Thread.Mutex = .{},
fileWatchCallbacks: std.ArrayListUnmanaged(WatchCallback) = .{},
pub const PakMounting = struct {
filePath: []const u8, // path to the file
bytes: []align(8) u8 = undefined,
@ -114,6 +126,48 @@ pub const PackerFS = struct {
}
};
pub fn addFileChangedCallback(
self: *@This(),
path: []const u8,
cb: *const fn ([]const u8, ?*anyopaque) void,
ctx: ?*anyopaque,
) !void {
const x = WatchCallback{
.func = cb,
.ctx = ctx,
.path = path,
};
try self.fileWatchCallbacks.append(self.allocator, x);
}
pub fn watchCallback(path: [*c]const u8, ctx: ?*anyopaque) callconv(.C) void {
const self: *@This() = @ptrCast(@alignCast(ctx));
// std.debug.print("WE GOT OURSELFS A FUCKIN CALLBAKC FOR A PATH MOTHERF- {s} {p} {d}\n", .{ path, ctx.?, self.pakMountings.items.len });
for (self.fileWatchCallbacks.items) |*watch| {
// std.debug.print("checking {s} {s}\n", .{ watch.path, path });
if (std.mem.eql(u8, watch.path, std.mem.span(path))) {
watch.call(std.mem.span(path));
}
// std.debug.print("registered callback: {s}\n", .{watch.path});
}
}
pub fn watchPath(self: *@This(), path: []const u8) void {
// std.debug.print("self = {x}\n", .{@intFromPtr(self)});
const p = self.stringArena.allocator().dupeZ(u8, path) catch return;
const watch = watcher.createWatchPoint(
p,
watchCallback,
self,
) orelse {
std.debug.print("Unable to create watch point, {s}\n", .{p});
return;
};
_ = watch;
}
// todo: implement load memory to file then-map setup.
// for systems which do not support mmap
@ -164,6 +218,26 @@ pub const PackerFS = struct {
return @intCast(self.fileHeaders.items.len);
}
pub fn resolveContentFile(self: *@This(), allocator: std.mem.Allocator, path: []const u8) !?[]u8 {
for (self.contentPaths.items) |contentPath| {
// std.debug.print("{s}\n", .{contentPath});
const fullPath = try std.fmt.allocPrint(self.stringArena.allocator(), "{s}/{s}", .{ contentPath, path });
defer self.stringArena.allocator().free(fullPath);
var exists: bool = true;
std.fs.cwd().access(fullPath, .{}) catch |err| {
exists = if (err == error.FileNotFound) false else true;
};
if (exists) {
return try allocator.dupe(u8, fullPath);
}
}
return null;
}
pub fn fileExists(self: *@This(), path: []const u8) bool {
self.lock.lock();
defer self.lock.unlock();
@ -231,6 +305,7 @@ pub const PackerFS = struct {
return self.stringArena.allocator();
}
// if skipLoading is set bytes will contain an empty path
fn loadFileDirect(self: *@This(), basePath: []const u8, path: []const u8) !?PackerBytesMapping {
// if we made it to this function we can assume that the file does not exist in existing pak mounting references.
// nor does it exist in self.fileHandlesByName
@ -347,6 +422,7 @@ pub const PackerFS = struct {
self.contentPaths.deinit(self.allocator);
self.stringArena.deinit();
self.lock.unlock();
self.fileWatchCallbacks.deinit(self.allocator);
self.allocator.destroy(self);
}
@ -414,3 +490,5 @@ pub const PackerFS = struct {
return .{ .allocator = allocator, .data = rv, .sources = sourcePath };
}
};
const watcher = @import("watcher.zig");

17
lib/packer/src/watcher.cpp vendored Normal file
View File

@ -0,0 +1,17 @@
#include <FileWatch.hpp>
extern "C" void* createWatchPoint(
const char* watchPath,
void(*cb)(const char*, void*),
void* ctx
){
auto* watch = new filewatch::FileWatch<std::string>(
watchPath,
[cb, ctx] (const std::string& path, const filewatch::Event change_type) {
std::cout << event_to_string(change_type) << std::endl;
cb(path.c_str(), ctx);
}
);
return watch;
}

7
lib/packer/src/watcher.zig vendored Normal file
View File

@ -0,0 +1,7 @@
pub const CallbackType = *const fn ([*c]const u8, ?*anyopaque) callconv(.C) void;
pub extern fn createWatchPoint(
[*c]const u8,
CallbackType,
?*anyopaque,
) callconv(.C) ?*anyopaque;

View File

@ -116,3 +116,27 @@ test "packerfs_test" {
std.debug.print("littleEndian {any}", .{packer.littleEndian});
}
fn fileChangedCb(pathChanged: []const u8, ctx: ?*anyopaque) void {
_ = ctx;
std.debug.print("pathChanged {s}\n", .{pathChanged});
}
test "packer file watch" {
var fs = try PackerFS.init(std.testing.allocator, .{});
defer fs.destroy();
std.fs.cwd().deleteFile("test_output/test2.txt") catch {};
try fs.addContentPath("test_output");
fs.watchPath("test_output");
try fs.addFileChangedCallback("test2.txt", fileChangedCb, null);
var file = try std.fs.cwd().createFile("test_output/test2.txt", .{});
std.time.sleep(100000000);
std.debug.print("writing to file\n", .{});
// var file = try std.fs.cwd().openFile("test_output/test2.txt", .{ .mode = .read_write });
try file.writeAll("what the fuck bro");
std.time.sleep(400000000);
// std.time.sleep(5000000000);
}

View File

@ -21,4 +21,17 @@ pub fn build(b: *std.Build) void {
blbuild.addExtraModule(sampleGame, "videoplayer");
blbuild.addExtraModule(sampleGame, "doomplayer");
blbuild.addExtraModule(sampleGame, "bsp");
const sampleGameExtern = b.addSharedLibrary(.{
.root_source_file = b.path("sampleGame/externGame/externGame.zig"),
.link_libc = true,
.optimize = optimize,
.target = target,
.name = "externGame",
});
const installExtern = b.addInstallArtifact(sampleGameExtern, .{
.dest_dir = .{ .override = .{ .custom = "modules" } },
});
b.getInstallStep().dependOn(&installExtern.step);
}

View File

@ -0,0 +1,5 @@
pub export fn add(a: i32, b: i32) i32 {
return a + b + 32000;
}
const std = @import("std");

View File

@ -28,7 +28,9 @@ rendererDebugger: *extras.RendererDebug = undefined,
tbMap: ?*bsp.maploader.TBMap = null,
fileWatch: f64 = 3.0,
addFunc: ?*const fn (i32, i32) callconv(.C) i32 = undefined,
modules: std.AutoHashMapUnmanaged(u32, []const u8) = .{},
pub var NeonObjectTable: core.EngineObjectVTable = core.EngineObjectVTable.from(@This());
@ -172,6 +174,14 @@ pub fn loadMap(self: *@This()) void {
core.engine_log("map loaded", .{});
}
fn moduleChangedCallback(pathChanged: []const u8, ctx: ?*anyopaque) void {
const self: *@This() = @ptrCast(@alignCast(ctx));
core.engine_log("moduleChanged {s}", .{pathChanged});
var name = core.MakeName(pathChanged);
self.modules.put(self.allocator, name.handle(), name.utf8()) catch {};
}
pub fn prepare(self: *@This()) !void {
core.engine_log(">>>>>>> game prepare", .{});
var z = core.tracy.ZoneN(@src(), "PREPARING GAME");
@ -180,6 +190,11 @@ pub fn prepare(self: *@This()) !void {
try script.loadTypes("scripts");
try script.runScriptFile("scripts/prepare.lua");
core.fs().watchPath("zig-out/modules");
try core.fs().addFileChangedCallback(core.sharedLibName("externGame"), moduleChangedCallback, self);
try self.tryLoadExtern("externGame");
self.rendererDebugger = try extras.RendererDebug.create(self.allocator); //try core.createObject(extras.RendererDebug, .{});
self.objectSpawner = try extras.ObjectSpawner.create(self.allocator);
@ -361,17 +376,39 @@ pub fn getMouseMovement(self: *@This()) core.Vector2f {
return rv;
}
pub fn tryLoadExtern(self: *@This(), gameName: []const u8) !void {
var buf = std.mem.zeroes([256]u8);
const os_tag = @import("builtin").os.tag;
var suffix: []const u8 = "dll";
if (os_tag == .linux) {
suffix = "so";
} else if (os_tag == .macos) {
suffix = "dynlib";
}
const path = try std.fmt.bufPrint(&buf, "zig-out/bin/{s}.{s}", .{ gameName, suffix });
var lib = try std.DynLib.open(path);
// std.debug.print("path: {s}", .{path});
self.addFunc = lib.lookup(@TypeOf(self.addFunc.?), "add");
}
pub fn tick(self: *@This(), dt: f64) void {
const fdt: f32 = @floatCast(dt);
_ = ig.dockSpaceOverViewport(ig.getMainViewport(), .{ .passthru_central_node = true }, null);
self.loadMap2() catch unreachable;
// self.loadMap2() catch unreachable;
self.fileWatch -= dt;
if (self.fileWatch < 0) {
self.fileWatch = 3.0;
}
core.loopDelay(@src(), 1.0, dt, struct {
s: @TypeOf(self),
pub fn func(c: @This()) void {
c.s.tryLoadExtern("externGame") catch unreachable;
}
}, .{ .s = self });
const z1 = tracy.ZoneN(@src(), "inputDebugger");
if (!self.mouseLook) {
@ -441,6 +478,15 @@ pub fn tick(self: *@This(), dt: f64) void {
ig.textFmt("- f2 to route all inputs to the doom player", .{}) catch return;
ig.textFmt("- movingLight checkbox makes the light stop moving with you", .{}) catch return;
ig.textf("1 + 2 = {d}", .{self.addFunc.?(1, 2)});
{
ig.textf("modules dirty: ", .{});
var i = self.modules.iterator();
while (i.next()) |n| {
ig.textf("{s} pending reload", .{n.value_ptr.*});
}
}
if (ig.smallButton("reload map")) {
self.loadMap2() catch unreachable;
}
@ -472,6 +518,7 @@ pub fn tick(self: *@This(), dt: f64) void {
}
pub fn deinit(self: *@This()) void {
self.modules.deinit(self.allocator);
self.rendererDebugger.destroy();
DoomPlayer.DoomCanvas.cleanupDoom();
self.fpcamera.destroy();