diff --git a/engine/src/engine/asset_bundle.zig b/engine/src/engine/asset_bundle.zig index 9ae2872..278135a 100644 --- a/engine/src/engine/asset_bundle.zig +++ b/engine/src/engine/asset_bundle.zig @@ -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 }); } diff --git a/engine/src/engine/audio.zig b/engine/src/engine/audio.zig index cc6f0c5..c5c8657 100644 --- a/engine/src/engine/audio.zig +++ b/engine/src/engine/audio.zig @@ -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 { diff --git a/engine/src/engine/game_linking.zig b/engine/src/engine/game_linking.zig new file mode 100644 index 0000000..82b118f --- /dev/null +++ b/engine/src/engine/game_linking.zig @@ -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; + } +} diff --git a/engine/src/engine/root.zig b/engine/src/engine/root.zig index 1606c96..f6ae6d4 100644 --- a/engine/src/engine/root.zig +++ b/engine/src/engine/root.zig @@ -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 }); diff --git a/engine/src/game_lib/asset.zig b/engine/src/game_lib/asset.zig index 80fd419..767dd22 100644 --- a/engine/src/game_lib/asset.zig +++ b/engine/src/game_lib/asset.zig @@ -115,6 +115,7 @@ pub const Loader = struct { image_info = .{ .width = image.width, .height = image.height, + .format = .png }; } diff --git a/engine/src/game_lib/font.zig b/engine/src/game_lib/font.zig index 6a2d846..f912142 100644 --- a/engine/src/game_lib/font.zig +++ b/engine/src/game_lib/font.zig @@ -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) { _, diff --git a/engine/src/game_lib/sound.zig b/engine/src/game_lib/sound.zig index 6a2d846..49e8500 100644 --- a/engine/src/game_lib/sound.zig +++ b/engine/src/game_lib/sound.zig @@ -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) { _, diff --git a/engine/src/game_lib/texture.zig b/engine/src/game_lib/texture.zig index 661ee7d..e3ed18e 100644 --- a/engine/src/game_lib/texture.zig +++ b/engine/src/game_lib/texture.zig @@ -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) { _,