From 5e9b4b43a9d2f75c8e5479a62d2a2fb83ba0fbd5 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Sun, 12 Jul 2026 20:03:52 +0300 Subject: [PATCH] give up on this repo, rewrite on v2 without hot code reload --- .gitignore | 1 + engine/build.zig | 9 +- engine/src/engine/environment.zig | 5 +- engine/src/engine/graphics.zig | 27 +++- engine/src/engine/root.zig | 210 ++++++++++++++++++++++++++++-- engine/src/game_lib/root.zig | 28 +++- engine/src/runtime/main.zig | 24 +++- src/assets/icon.image.ini | 4 + src/game.zig | 24 ++++ 9 files changed, 308 insertions(+), 24 deletions(-) create mode 100644 src/assets/icon.image.ini diff --git a/.gitignore b/.gitignore index 880cd5d..7e5c1f7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ zig-out +zig-pkg .zig-cache diff --git a/engine/build.zig b/engine/build.zig index 4eb1425..ac27787 100644 --- a/engine/build.zig +++ b/engine/build.zig @@ -48,9 +48,6 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void { build_options.addOption(bool, "code_dynamic_linking", code_dynamic_linking); build_options.addOption(bool, "code_static_linking", code_static_linking); build_options.addOption(bool, "asset_hot_reload", asset_hot_reload); - if (asset_hot_reload) { - build_options.addOptionPath("asset_dir", opts.asset_dir); - } const engine_lib = b.createModule(.{ .root_source_file = b.path("src/game_lib/root.zig") @@ -140,13 +137,17 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void { }); const install_game_lib = b.addInstallArtifact(game_lib, .{}); - run_cmd.addArg("--game-lib-path"); + run_cmd.addArg("--dynamic-lib-path"); 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); } + if (asset_hot_reload) { + run_cmd.addArg("--assets-path"); + run_cmd.addFileArg(opts.asset_dir); + } run_cmd_step = &run_cmd.step; } } diff --git a/engine/src/engine/environment.zig b/engine/src/engine/environment.zig index 328c188..e2f6707 100644 --- a/engine/src/engine/environment.zig +++ b/engine/src/engine/environment.zig @@ -106,15 +106,16 @@ fn processEvent(self: *Environment, event: Input.Event) void { } } -pub fn stateInit(self: *Environment, initial: Init) void { +pub fn stateInit(self: *Environment, initial: Init, assets: Lib.Asset.List) void { assert(self.frame == null); const opts = Lib.Init{ .gpa = self.gpa, .seed = initial.seed, + .assets = assets }; self.state = self.callbacks.init(&opts); - self.frame = .init(self.gpa, initial.screen_size); + self.frame = .init(self.gpa, initial.screen_size, assets); } pub fn stateDeinit(self: *Environment) void { diff --git a/engine/src/engine/graphics.zig b/engine/src/engine/graphics.zig index b0f9135..92e4898 100644 --- a/engine/src/engine/graphics.zig +++ b/engine/src/engine/graphics.zig @@ -58,6 +58,18 @@ const Texture = struct { width: u32, height: u32, rgba: [*]u8, + + pub fn clone(self: Data, allocator: std.mem.Allocator) !Data { + const pixel_count = self.width*self.height; + const rgba = try allocator.dupe(u8, self.rgba[0..(pixel_count * 4)]); + errdefer allocator.free(rgba); + + return Data{ + .width = self.width, + .height = self.height, + .rgba = rgba.ptr, + }; + } }; fn destroy(self: *Texture) void { @@ -103,6 +115,9 @@ scissor_stack: std.ArrayList(Rect) = .empty, sokol_state: ?SokolState, sokol_options: SokolOptions, +deferred_textures_arena: std.heap.ArenaAllocator, +deferred_textures: std.AutoArrayHashMapUnmanaged(Lib.Texture.Id, Texture.Data) = .empty, + const Options = struct { allocator: std.mem.Allocator, logger: sg.Logger = .{}, @@ -113,6 +128,7 @@ pub fn init(options: Options) !Graphics { return Graphics{ .gpa = options.allocator, .sokol_state = null, + .deferred_textures_arena = std.heap.ArenaAllocator.init(options.allocator), .sokol_options = .{ .logger = options.logger, .imgui_font = options.imgui_font, @@ -198,6 +214,13 @@ pub fn setupSokol(self: *Graphics) !void { .nearest_sampler = nearest_sampler, .font_context = font_context, }; + + var iter = self.deferred_textures.iterator(); + while (iter.next()) |entry| { + try self.setTextureData(entry.key_ptr.*, entry.value_ptr.*); + } + _ = self.deferred_textures_arena.reset(.free_all); + self.deferred_textures = .empty; } pub fn deinit(self: *Graphics, gpa: std.mem.Allocator) void { @@ -525,7 +548,9 @@ pub fn removeTexture(self: *Graphics, id: Lib.Texture.Id) void { pub fn setTextureData(self: *Graphics, id: Lib.Texture.Id, data: Texture.Data) !void { if (self.sokol_state == null) { - return error.SokolNotSetup; + const arena = self.deferred_textures_arena.allocator(); + try self.deferred_textures.put(arena, id, try data.clone(arena)); + return; } const texture = self.textures.get(.fromInt(@intFromEnum(id))) orelse return; diff --git a/engine/src/engine/root.zig b/engine/src/engine/root.zig index b088a2a..7bb7c0d 100644 --- a/engine/src/engine/root.zig +++ b/engine/src/engine/root.zig @@ -36,14 +36,126 @@ const Environment = @import("environment.zig"); const Engine = @This(); +const IniReader = struct { + reader: *std.Io.Reader, + + const Tag = union(enum) { + section: []const u8, + option: struct { + key: []const u8, + value: []const u8 + }, + + pub fn isOption(self: Tag, key: []const u8) ?[]const u8 { + if (self == .option and std.mem.eql(u8, self.option.key, key)) { + return self.option.value; + } + return null; + } + }; + + pub fn next(self: *IniReader) !?Tag { + while (true) { + var line = self.reader.takeDelimiterInclusive('\n') catch |e| switch (e) { + error.EndOfStream => break, + else => return e + }; + line = @constCast(std.mem.trim(u8, line, &std.ascii.whitespace)); + if (std.mem.startsWith(u8, line, "#")) { + continue; + } + if (line.len == 0) { + continue; + } + + if (std.mem.startsWith(u8, line, "[") and std.mem.endsWith(u8, line, "]")) { + const section_name = line[1..(line.len-1)]; + return Tag{ + .section = std.mem.trim(u8, section_name, &std.ascii.whitespace) + }; + } + if (std.mem.indexOfScalar(u8, line, '=')) |equals_index| { + const key = line[0..equals_index]; + const value = line[(equals_index+1)..]; + + return Tag{ + .option = .{ + .key = std.mem.trim(u8, key, &std.ascii.whitespace), + .value = std.mem.trim(u8, value, &std.ascii.whitespace), + } + }; + } + + return error.InvalidLine; + } + return null; + } +}; + +pub const LoadOptions = struct { + asset_dir: std.fs.Dir, + ini_reader: *IniReader, + arena: *std.heap.ArenaAllocator +}; + +pub const AssetSystem = struct { + assets: std.ArrayList(Lib.Asset), + + pub fn register(self: AssetSystem) void { + _ = self; // autofix + } +}; + +pub const Subsystems = struct { + graphics: *Graphics, + audio: *Audio, + assets: *AssetSystem, +}; + +const ImageAsset = struct { + pub fn load(subsystems: Subsystems, opts: LoadOptions) !void { + const ini_reader = opts.ini_reader; + const arena = opts.arena; + const dir = opts.asset_dir; + + var maybe_image_path: ?[]const u8 = null; + while (try ini_reader.next()) |tag| { + if (tag.isOption("path")) |opt| { + maybe_image_path = try arena.allocator().dupe(u8, opt); + } + } + + if (maybe_image_path) |image_path| { + const image_data = try readFileAll(arena.allocator(), dir, image_path); + const image = try STBImage.load(image_data); + defer image.deinit(); + std.debug.print("{s}\n", .{image_path}); + const texture_id = try subsystems.graphics.addTexture(); + try subsystems.graphics.setTextureData(texture_id, .{ + .width = image.width, + .height = image.height, + .rgba = image.rgba8_pixels, + }); + } + // subsystems.assets.register("icon"); + // try subsystems.assets.append(self.assets_arena.allocator(), Lib.Asset{ + // .id = @enumFromInt(self.assets.items.len), + // .key = try self.assets_arena.allocator().dupe(u8, asset_id), + // .data = &[_]u8{} + // }); + } +}; + allocator: std.mem.Allocator, graphics: Graphics, audio: Audio, seed: u64, -// game_linking: GameLinking, environment: Environment, +assets_arena: std.heap.ArenaAllocator, +assets: std.ArrayList(Lib.Asset), + queued_events: std.ArrayList(Input.Event), is_mouse_inside: bool, @@ -51,28 +163,23 @@ debug: DebugSystem, pub const RunOptions = struct { allocator: std.mem.Allocator, - game_library_path: ?[]const u8 = null, do_recording: bool = (builtin.mode == .Debug), screen_width: u32 = 640, screen_height: u32 = 480, + + dynamic_library_path: ?[]const u8 = null, + assets_path: ?[]const u8 = null, }; pub fn run(self: *Engine, opts: RunOptions) !void { - // var game_linking: GameLinking = undefined; - // if (opts.game_library_path) |game_library_path| { - // game_linking = try .initDynamic(opts.allocator, game_library_path); - // } else { - // game_linking = try .initStatic(); - // } - var debug_system = DebugSystem.init(opts.allocator); var maybe_callbacks: ?GameCallbacks = null; if (build_options.code_static_linking) { maybe_callbacks = GameLinking.getStatic(); } - if (build_options.code_dynamic_linking and opts.game_library_path != null) { - var dynamic = try GameLinking.Dynamic.init(opts.allocator, opts.game_library_path.?); + if (build_options.code_dynamic_linking and opts.dynamic_library_path != null) { + var dynamic = try GameLinking.Dynamic.init(opts.allocator, opts.dynamic_library_path.?); maybe_callbacks = try dynamic.getCallbacks(); debug_system.dynamic_linking = dynamic; } @@ -105,10 +212,62 @@ pub fn run(self: *Engine, opts: RunOptions) !void { .queued_events = .empty, .seed = initial.seed, .is_mouse_inside = false, - .debug = debug_system + .debug = debug_system, + .assets = .empty, + .assets_arena = std.heap.ArenaAllocator.init(opts.allocator) }; - self.environment.stateInit(initial); + if (opts.assets_path) |assets_path| { + var temp_arena = std.heap.ArenaAllocator.init(opts.allocator); + defer temp_arena.deinit(); + + var result_arena = std.heap.ArenaAllocator.init(opts.allocator); + defer result_arena.deinit(); + + var dir = try std.fs.openDirAbsolute(assets_path, .{ .iterate = false }); + defer dir.close(); + + const result = try listFilesRecursively(assets_path, temp_arena.allocator(), result_arena.allocator(), ".ini"); + for (result.items) |path| { + const path_without_ini = path[0..(path.len-4)]; + const index_of_dot = std.mem.lastIndexOfScalar(u8, path_without_ini, '.') orelse continue; + const asset_type = path_without_ini[(index_of_dot+1)..]; + const asset_id = path_without_ini[0..index_of_dot]; + + std.debug.print("{s} {s}\n", .{asset_id, asset_type}); + if (std.mem.eql(u8, asset_type, "image")) { + const f = try dir.openFile(path, .{ }); + defer f.close(); + + var reader_buffer: [Math.bytes_per_kib * 4]u8 = undefined; + var file_reader = f.reader(&reader_buffer); + var ini_reader = IniReader{ .reader = &file_reader.interface }; + + var asset_system = AssetSystem{ + .assets = .empty + }; + try ImageAsset.load(.{ + .audio = &self.audio, + .graphics = &self.graphics, + .assets = &asset_system + }, + .{ + .arena = &temp_arena, + .ini_reader = &ini_reader, + .asset_dir = dir + }); + } + } + // for (result) + } + + try self.assets.append(self.allocator, Lib.Asset{ + .data = &.{} + }); + + self.environment.stateInit(initial, .{ + .items = self.assets.items + }); // TODO: // if (opts.do_recording) { @@ -169,6 +328,30 @@ pub fn run(self: *Engine, opts: RunOptions) !void { }); } +fn listFilesRecursively( + absolute_path: []const u8, + temp_allocator: std.mem.Allocator, + result_arena: std.mem.Allocator, + extension: []const u8 +) !std.ArrayList([]u8) { + var result: std.ArrayList([]u8) = .empty; + var dir = try std.fs.openDirAbsolute(absolute_path, .{ .iterate = true }); + defer dir.close(); + + var walker = try dir.walk(temp_allocator); + defer walker.deinit(); + + while (try walker.next()) |entry| { + if (entry.kind == .file) { + if (std.mem.endsWith(u8, entry.path, extension)) { + try result.append(result_arena, try result_arena.dupe(u8, entry.path)); + } + } + } + + return result; +} + fn readFileAll(gpa: std.mem.Allocator, dir: std.fs.Dir, sub_path: []const u8) ![]u8 { const f = try dir.openFile(sub_path, .{}); defer f.close(); @@ -206,6 +389,7 @@ fn sokolCleanup(self: *Engine) void { self.audio.deinit(); self.graphics.deinit(self.allocator); self.debug.deinit(); + self.assets_arena.deinit(); // TODO: // self.game_linking.deinit(self.allocator); } diff --git a/engine/src/game_lib/root.zig b/engine/src/game_lib/root.zig index ea51019..647b8ba 100644 --- a/engine/src/game_lib/root.zig +++ b/engine/src/game_lib/root.zig @@ -270,6 +270,27 @@ pub const KeyState = struct { } }; +pub const Asset = struct { + data: []const u8, + // type: *std.builtin.Type, + + pub const Id = u16; + + pub const List = struct { + items: []Asset, + + pub fn get(self: List, comptime T: type, id: Id) *const T { + if (id >= self.items.len) { + @panic("foo"); + } + if (@sizeOf(T) != self.items[id].data.len) { + @panic("foo"); + } + return @ptrCast(@alignCast(self.items[id].data.ptr)); + } + }; +}; + pub const Frame = struct { arena: std.heap.ArenaAllocator, time_ns: Nanoseconds, @@ -288,9 +309,12 @@ pub const Frame = struct { screen_size: Vec2, clear_color: Vec4, + assets: Asset.List, + pub fn init( gpa: std.mem.Allocator, - screen_size: Vec2 + screen_size: Vec2, + assets: Asset.List, ) Frame { return Frame{ .arena = std.heap.ArenaAllocator.init(gpa), @@ -306,6 +330,7 @@ pub const Frame = struct { .audio_commands = .empty, .hide_cursor = false, .screen_size = screen_size, + .assets = assets }; } @@ -473,6 +498,7 @@ pub const Frame = struct { pub const Init = struct { gpa: std.mem.Allocator, seed: u64, + assets: Asset.List, }; pub const Callbacks = struct { diff --git a/engine/src/runtime/main.zig b/engine/src/runtime/main.zig index 48f7ee7..50c0371 100644 --- a/engine/src/runtime/main.zig +++ b/engine/src/runtime/main.zig @@ -29,6 +29,9 @@ pub fn main() !void { var game_library_path: ?[]const u8 = null; defer if (game_library_path) |str| allocator.free(str); + var assets_path: ?[]const u8 = null; + defer if (assets_path) |str| allocator.free(str); + // var asset_loading: Engine.RunOptions.AssetLoading = undefined; // if (!build_options.asset_hot_reload) { // asset_loading = .{ @@ -53,12 +56,21 @@ pub fn main() !void { var opt_game_lib_path: ?CLI.Option.Id = null; if (build_options.code_dynamic_linking) { opt_game_lib_path = try cli.addOption(.{ - .long_name = "game-lib-path", + .long_name = "dynamic-lib-path", .description = "Load game code from dynamic library", .kind = .single_argument }); } + var opt_assets_path: ?CLI.Option.Id = null; + if (build_options.asset_hot_reload) { + opt_assets_path = try cli.addOption(.{ + .long_name = "assets-path", + .description = "Load assets from folder at runtime", + .kind = .single_argument + }); + } + const cmd_version = try cli.addCommand(.{ .name = "version", .description = "Show semantic version and build options" @@ -85,6 +97,12 @@ pub fn main() !void { } } + if (opt_assets_path) |opt| { + if (parsed.getOptionArgument(opt)) |arg| { + assets_path = try allocator.dupe(u8, arg); + } + } + const command = parsed.command orelse cmd_launch; if (command == cmd_version) { cli.stdout.write("Version: {s}\n", .{ "TODO:" }); @@ -102,8 +120,8 @@ pub fn main() !void { try engine.run(.{ .allocator = allocator, - .game_library_path = game_library_path, - // .asset_loading = asset_loading + .dynamic_library_path = game_library_path, + .assets_path = assets_path }); } diff --git a/src/assets/icon.image.ini b/src/assets/icon.image.ini new file mode 100644 index 0000000..cfa3c25 --- /dev/null +++ b/src/assets/icon.image.ini @@ -0,0 +1,4 @@ +[asdf] +path = ./icon.png + +# afd diff --git a/src/game.zig b/src/game.zig index db97dca..1a7e9c2 100644 --- a/src/game.zig +++ b/src/game.zig @@ -14,6 +14,22 @@ const State = struct { player: Vec2, }; +const AssetId = enum { + icon +}; + +const Image = struct { + width: u32, + height: u32, + rgba: []const u8, + + pub const nil = Image{ + .width = 0, + .height = 0, + .rgba = &.{ } + }; +}; + pub fn init(opts: *const Engine.Init) callconv(.c) *anyopaque { const gpa = opts.gpa; @@ -23,6 +39,14 @@ pub fn init(opts: *const Engine.Init) callconv(.c) *anyopaque { .player = .init(50, 50), }; + // var icon = opts.assets.get(ImageAsset, "icon"); + // icon.id + // icon.rgba + // icon.texture_id + + // std.debug.print("{}\n", .{opts.assets}); + // const icon = opts.assets.get(Image, @intFromEnum(AssetId.icon)); + return state; }