move GameLinking struct to separate file

This commit is contained in:
Rokas Puzonas 2026-02-16 23:36:03 +02:00
parent 483daaf91e
commit e995b55ccc
8 changed files with 327 additions and 250 deletions

View File

@ -15,11 +15,13 @@ const DataSegment = extern struct {
const Font = extern struct {
asset_id: Lib.Asset.Id,
info: Lib.Font.Info,
data: DataSegment
};
const Sound = extern struct {
asset_id: Lib.Asset.Id,
info: Lib.Sound.Info,
data: DataSegment
};
@ -82,11 +84,13 @@ pub fn appendSound(
self: *AssetBundle,
gpa: std.mem.Allocator,
id: Lib.Asset.Id,
data: []const u8
data: []const u8,
info: Lib.Sound.Info,
) !void {
try self.sounds.append(gpa, Sound{
.asset_id = id,
.data = try self.insertData(gpa, data)
.data = try self.insertData(gpa, data),
.info = info
});
}
@ -94,11 +98,13 @@ pub fn appendFont(
self: *AssetBundle,
gpa: std.mem.Allocator,
id: Lib.Asset.Id,
data: []const u8
data: []const u8,
info: Lib.Font.Info,
) !void {
try self.fonts.append(gpa, Font{
.asset_id = id,
.data = try self.insertData(gpa, data)
.data = try self.insertData(gpa, data),
.info = info
});
}

View File

@ -371,21 +371,12 @@ pub fn removeSound(self: *AudioSystem, id: Lib.Sound.Id) void {
}
pub const DataOptions = struct {
const PlaybackStyle = enum {
stream,
decode_once,
// If the decoded size is less than `stream_threshold`, then .decode_once will by default be used.
const stream_threshold = 10 * Math.bytes_per_mib;
// If the decoded size is less than `stream_threshold`, then .decode_once will by default be used.
const stream_threshold = 10 * Math.bytes_per_mib;
};
const Format = enum {
vorbis
};
format: Format,
format: Lib.Sound.Format,
data: []const u8,
playback_style: ?PlaybackStyle = null,
playback_style: ?Lib.Sound.PlaybackStyle = null,
};
pub fn setSoundData(self: *AudioSystem, id: Lib.Sound.Id, opts: DataOptions) !void {
@ -394,12 +385,11 @@ pub fn setSoundData(self: *AudioSystem, id: Lib.Sound.Id, opts: DataOptions) !vo
assert(opts.format == .vorbis);
const vorbis = try Sound.Vorbis.init(self.gpa, opts.data, self.temp_vorbis_alloc_buffer);
const PlaybackStyle = DataOptions.PlaybackStyle;
const PlaybackStyle = Lib.Sound.PlaybackStyle;
var playback_style: PlaybackStyle = undefined;
if (opts.playback_style == null) {
const stream_threshold = PlaybackStyle.stream_threshold;
const decoded_size = vorbis.getChannels() * vorbis.getSampleCount() * @sizeOf(f32);
const default_syle: PlaybackStyle = if (decoded_size < stream_threshold) .decode_once else .stream;
const default_syle: PlaybackStyle = if (decoded_size < DataOptions.stream_threshold) .decode_once else .stream;
playback_style = default_syle;
} else {

View File

@ -0,0 +1,266 @@
const std = @import("std");
const log = std.log.scoped(.engine);
const assert = std.debug.assert;
const Lib = @import("engine_lib");
const GameCallbacks = Lib.Callbacks;
const build_options = Lib.build_options;
const GameLinking = @This();
kind: union(enum) {
static,
dynamic: KindDynamic,
},
callbacks: GameCallbacks,
const KindDynamic = 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,
// TODO: Make this configurable
const event_debounce_ns = std.time.ns_per_ms * 150;
fn ensureLookup(lib: *std.DynLib, comptime T: type, name: [:0]const u8) !T {
return lib.lookup(T, name) orelse return error.MissingSymbol;
}
fn getCallbacksFromLibrary(lib: *std.DynLib) !GameCallbacks {
return GameCallbacks{
.init = try ensureLookup(lib, GameCallbacks.InitFn, "init"),
.tick = try ensureLookup(lib, GameCallbacks.TickFn, "tick"),
.deinit = try ensureLookup(lib, GameCallbacks.DeinitFn, "deinit"),
.loadAssets = if (build_options.load_assets_at_runtime) try ensureLookup(lib, GameCallbacks.LoadAssetsFn, "loadAssets") else {},
.debug = if (build_options.has_imgui) try ensureLookup(lib, GameCallbacks.DebugFn, "debug") else {},
};
}
fn init(gpa: std.mem.Allocator, path: []const u8) !KindDynamic {
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);
const lib = try std.DynLib.open(path);
return KindDynamic{
.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
};
}
fn deinit(self: *KindDynamic, gpa: std.mem.Allocator) void {
self.lib.close();
gpa.free(self.dir_path);
gpa.free(self.path);
std.posix.close(self.fan_fd);
}
fn getCallbacks(self: *KindDynamic) !GameCallbacks {
return getCallbacksFromLibrary(&self.lib);
}
fn checkForChanges(self: *KindDynamic) !?GameCallbacks {
const fanotify = std.os.linux.fanotify;
const M = fanotify.event_metadata;
const fan_fd = self.fan_fd;
var need_to_reload = false;
// TODO: clean this up.
// Reuse the same watching logic for assets
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.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.ignore_next_event) {
self.ignore_next_event = false;
} else {
self.last_event_at = try std.time.Instant.now();
}
}
if (self.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.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.path), self.reload_index}
);
try std.fs.Dir.copyFile(
std.fs.cwd(), self.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.lib.close();
self.lib = new_lib;
self.reload_index += 1;
self.ignore_next_event = true;
return new_callbacks;
}
}
return null;
}
};
const KindStatic = struct {
fn getCallbacks() GameCallbacks {
const game = @import("game");
return GameCallbacks{
.init = game.init,
.deinit = game.deinit,
.tick = game.tick,
.loadAssets = if (build_options.load_assets_at_runtime) game.loadAssets else {},
.debug = if (build_options.has_imgui) game.debug else {},
};
}
};
pub fn initDynamic(gpa: std.mem.Allocator, path: []const u8) !GameLinking {
if (!build_options.hot_reload) {
return error.NotSupported;
}
var dynamic = try KindDynamic.init(gpa, path);
errdefer dynamic.deinit(gpa);
const callbacks = try dynamic.getCallbacks();
return GameLinking{
.kind = .{ .dynamic = dynamic },
.callbacks = callbacks
};
}
pub fn initStatic() !GameLinking {
if (!build_options.statically_linked) {
return error.NotStaticallyLinked;
}
return GameLinking{
.kind = .static,
.callbacks = KindStatic.getCallbacks()
};
}
pub fn deinit(self: *GameLinking, gpa: std.mem.Allocator) void {
if (build_options.hot_reload and self.kind == .dynamic) {
self.kind.dynamic.deinit(gpa);
}
}
pub fn check(self: *GameLinking) !void {
if (self.kind == .static) {
return;
}
if (!build_options.hot_reload) {
return;
}
if (try self.kind.dynamic.checkForChanges()) |new_callbacks| {
self.callbacks = new_callbacks;
}
}

View File

@ -28,233 +28,7 @@ pub const Vec2 = Math.Vec2;
const rgb = Math.rgb;
const Vec4 = Math.Vec4;
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,
.loadAssets = if (build_options.load_assets_at_runtime) game.loadAssets else {},
.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 ensureLookup(lib: *std.DynLib, comptime T: type, name: [:0]const u8) !T {
return lib.lookup(T, name) orelse return error.MissingSymbol;
}
fn getCallbacksFromLibrary(lib: *std.DynLib) !GameCallbacks {
return GameCallbacks{
.init = try ensureLookup(lib, GameCallbacks.InitFn, "init"),
.tick = try ensureLookup(lib, GameCallbacks.TickFn, "tick"),
.deinit = try ensureLookup(lib, GameCallbacks.DeinitFn, "deinit"),
.loadAssets = if (build_options.load_assets_at_runtime) try ensureLookup(lib, GameCallbacks.LoadAssetsFn, "loadAssets") else {},
.debug = if (build_options.has_imgui) try ensureLookup(lib, GameCallbacks.DebugFn, "debug") 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;
// TODO: clean this up.
// Reuse the same watching logic for assets
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 GameLinking = @import("game_linking.zig");
const GameState = struct {
gpa: std.mem.Allocator,
@ -1072,8 +846,17 @@ fn showDebugWindow(self: *Engine) !void {
}
}
const linking_label = if (self.game_linking.kind == .static) "static" else "dynamic";
imgui.textFmt("Linking: {s}", .{ linking_label });
if (self.game_linking.kind == .dynamic) {
imgui.textFmt("Linking: dynamic (from {s})", .{ self.game_linking.kind.dynamic.path });
} else {
imgui.textFmt("Linking: static", .{ });
}
if (build_options.load_assets_at_runtime) {
imgui.textFmt("Asset loading: runtime (from {s})", .{build_options.asset_dir});
} else {
imgui.textFmt("Asset loading: builtin", .{});
}
imgui.textFmt("Seed: 0x{x:08}", .{ self.seed });

View File

@ -115,6 +115,7 @@ pub const Loader = struct {
image_info = .{
.width = image.width,
.height = image.height,
.format = .png
};
}

View File

@ -1,6 +1,15 @@
const std = @import("std");
id: Id,
info: Info,
pub const Info = extern struct {
format: Format,
};
pub const Format = enum(u8) {
ttf
};
pub const Id = enum(u16) {
_,

View File

@ -1,6 +1,21 @@
const std = @import("std");
id: Id,
info: Info,
pub const Info = extern struct {
format: Format,
playback_style: PlaybackStyle
};
pub const PlaybackStyle = enum(u8) {
stream,
decode_once,
};
pub const Format = enum(u8) {
vorbis
};
pub const Id = enum(u16) {
_,

View File

@ -6,13 +6,20 @@ info: Info,
pub const Info = extern struct {
width: u32,
height: u32,
format: Format,
pub const nil = Info{
.width = 0,
.height = 0
.height = 0,
.format = .rgba
};
};
pub const Format = enum(u8) {
png,
rgba
};
pub const Id = enum(u16) {
_,