From 7197b68637d42754f166dba7b321837ee084b645 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Sat, 11 Jul 2026 21:50:54 +0300 Subject: [PATCH] add embedded assets --- build.zig | 48 ++++++++++++++++++++++ src/app.zig | 9 +---- src/platform.zig | 103 +++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 148 insertions(+), 12 deletions(-) diff --git a/build.zig b/build.zig index e454659..86a0a0e 100644 --- a/build.zig +++ b/build.zig @@ -5,10 +5,18 @@ pub fn build(b: *std.Build) !void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); + const assets_dir = "assets"; + const asset_files = [_][]const u8{ + "sokoban/sokoban_tilesheet.png" + }; + + const asset_dir_realpath = try b.build_root.join(b.graph.arena, &.{ assets_dir }); + const is_wasm = target.result.cpu.arch.isWasm(); const is_debug = (optimize == .Debug); const has_imgui = b.option(bool, "imgui", "ImGui integration") orelse is_debug; + const assets_from_filesystem = b.option(bool, "assets-filesystem", "Allow reading assets from filesystem") orelse is_debug; var has_tracy = false; if (!is_wasm) { @@ -23,8 +31,11 @@ pub fn build(b: *std.Build) !void { const build_options = b.addOptions(); build_options.addOption(bool, "has_imgui", has_imgui); + build_options.addOption(?[]const u8, "assets_dir", if (assets_from_filesystem) asset_dir_realpath else null); exe_mod.addOptions("build_options", build_options); + exe_mod.addImport("embedded_assets", try createEmbeddedAssets(b, assets_dir, &asset_files)); + const math_mod = b.createModule(.{ .root_source_file = b.path("src/math.zig") }); @@ -129,6 +140,43 @@ pub fn build(b: *std.Build) !void { run_step.dependOn(run_cmd_step); } +fn createEmbeddedAssets( + b: *std.Build, + assets_dir: []const u8, + asset_files: []const []const u8 +) !*std.Build.Module { + const write_files = b.addWriteFiles(); + + var embedded_assets: std.ArrayList(u8) = .empty; + defer embedded_assets.deinit(b.allocator); + + try embedded_assets.appendSlice(b.allocator, "pub const files = .{\n"); + const mod_embedded_assets = b.createModule(.{}); + for (asset_files) |asset_file| { + mod_embedded_assets.addAnonymousImport(asset_file, .{ + .root_source_file = b.path(b.pathJoin(&.{ assets_dir, asset_file })) + }); + + try embedded_assets.appendSlice(b.allocator, " .{ "); + + try embedded_assets.appendSlice(b.allocator, ".name = \""); + try embedded_assets.appendSlice(b.allocator, asset_file); + try embedded_assets.appendSlice(b.allocator, "\""); + + try embedded_assets.appendSlice(b.allocator, ", "); + + try embedded_assets.appendSlice(b.allocator, ".contents = @embedFile(\""); + try embedded_assets.appendSlice(b.allocator, asset_file); + try embedded_assets.appendSlice(b.allocator, "\")"); + + try embedded_assets.appendSlice(b.allocator, " },\n"); + } + try embedded_assets.appendSlice(b.allocator, "};\n"); + mod_embedded_assets.root_source_file = write_files.add("embedded_assets.zig", embedded_assets.items); + + return mod_embedded_assets; +} + const BuildNativeOptions = struct { name: []const u8, root_module: *std.Build.Module, diff --git a/src/app.zig b/src/app.zig index 694313a..b288001 100644 --- a/src/app.zig +++ b/src/app.zig @@ -22,14 +22,7 @@ sprite_sprite: Gfx.Sprite.Id, wall_sprite: Gfx.Sprite.Id, pub fn init(self: *App, plt: Platform.Init) !?u8 { - const cwd = std.Io.Dir.cwd(); - const sokoban_spritesheet = try cwd.readFileAlloc( - plt.io, - "assets/sokoban/sokoban_tilesheet.png", - plt.gpa, - .unlimited - ); - defer plt.gpa.free(sokoban_spritesheet); + const sokoban_spritesheet = plt.assets.readFile("sokoban/sokoban_tilesheet.png"); var image = try STBImage.load(sokoban_spritesheet); defer image.deinit(); diff --git a/src/platform.zig b/src/platform.zig index d09dfdd..2157113 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -27,6 +27,8 @@ pub const ImGUI = @import("./imgui.zig"); pub const Gfx = @import("./graphics.zig"); pub const Input = @import("./input.zig"); +const EmbeddedAssets = @import("embedded_assets"); + fn PlatformType(App: type) type { return struct { const Self = @This(); @@ -40,6 +42,7 @@ fn PlatformType(App: type) type { started_at: Io.Timestamp, last_frame_at: Io.Timestamp, + assets: Assets, input: Input, events_buffer: [256]Input.Event, events_overflow: bool, @@ -68,7 +71,8 @@ fn PlatformType(App: type) type { if (@hasDecl(App, "init")) { const plt = Init{ .gpa = self.gpa, - .io = self.io + .io = self.io, + .assets = &self.assets }; if (try self.app.init(plt)) |exit_code| { std.process.exit(exit_code); @@ -114,7 +118,8 @@ fn PlatformType(App: type) type { .io = self.io, .input = &self.input, .input_events = self.events.items, - .frame = self.frame_arena.allocator() + .frame = self.frame_arena.allocator(), + .assets = &self.assets }; try self.app.frame(plt); } @@ -395,6 +400,7 @@ fn PlatformType(App: type) type { self.app.deinit(plt); } self.frame_arena.deinit(); + self.assets.deinit(); ImGUI.deinit(); Gfx.deinit(); @@ -410,9 +416,82 @@ pub const RunOptions = struct { height: i32 = 600, }; +pub const Assets = struct { + arena: std.heap.ArenaAllocator, + gpa: Allocator, + io: Io, + + dir: ?Io.Dir, + files: std.array_hash_map.String(File), + + fn init(gpa: Allocator, io: Io, dir: ?Io.Dir) Assets { + return Assets{ + .gpa = gpa, + .io = io, + .arena = .init(gpa), + .dir = dir, + .files = .empty + }; + } + + fn readFileFromAssetDir(self: *Assets, filename: []const u8) !?[]const u8 { + const dir = self.dir orelse return null; + + const arena = self.arena.allocator(); + const contents = try dir.readFileAlloc( + self.io, + filename, + arena, + .unlimited + ); + + const filename_owned = try arena.dupe(u8, filename); + try self.files.put(self.gpa, filename_owned, File{ + .contents = contents + }); + + return contents; + } + + pub fn readFile(self: *Assets, filename: []const u8) []const u8 { + if (self.files.get(filename)) |file| { + return file.contents; + } else { + if (self.readFileFromAssetDir(filename)) |maybe_contents| { + if (maybe_contents) |contents| { + log.warn("Read file from which isn't defined in assets: '{s}'", .{filename}); + return contents; + } else { + log.err("Attempt to read file '{s}', which doesn't exist", .{filename}); + return ""; + } + } else |err| { + log.err("Failed to read file from filesystem: {}", .{err}); + return ""; + } + } + } + + fn insertStaticFile(self: *Assets, filename: []const u8, contents: []const u8) !void { + try self.files.put(self.gpa, filename, File{ + .contents = contents + }); + } + + fn deinit(self: *Assets) void { + self.arena.deinit(); + self.files.deinit(self.gpa); + } + + const File = struct { + contents: []const u8 + }; +}; + pub const Init = struct { gpa: Allocator, io: Io, + assets: *Assets }; pub const Deinit = struct { @@ -427,6 +506,7 @@ pub const Frame = struct { frame: Allocator, gpa: Allocator, io: Io, + assets: *Assets, input: *Input, input_events: []Input.Event, @@ -446,12 +526,22 @@ pub const Frame = struct { pub fn run(App: type, opts: RunOptions) void { const arena = opts.init.arena.allocator(); const gpa = opts.init.gpa; + const io = opts.init.io; + + var assets_dir: ?Io.Dir = null; + if (build_options.assets_dir) |assets_dir_path| { + const cwd = Io.Dir.cwd(); + assets_dir = cwd.openDir(io, assets_dir_path, .{}) catch |e| blk: { + log.err("Failed to open assets directory: {}", .{e}); + break :blk null; + }; + } const PlatformApp = PlatformType(App); const plt = arena.create(PlatformApp) catch @panic("OOM"); plt.* = PlatformApp{ .gpa = gpa, - .io = opts.init.io, + .io = io, .cli_args = opts.init.minimal.args.toSlice(arena) catch @panic("OOM"), .app = undefined, .input = undefined, @@ -463,9 +553,14 @@ pub fn run(App: type, opts: RunOptions) void { .last_key_press_is_repeat = false, .last_key_pressed = null, .frame_arena = .init(gpa), - .show_imgui = builtin.mode == .Debug + .show_imgui = builtin.mode == .Debug, + .assets = .init(gpa, io, assets_dir) }; + inline for (EmbeddedAssets.files) |file| { + plt.assets.insertStaticFile(file.name, file.contents) catch @panic("OOM"); + } + var icon: sapp.IconDesc = .{}; icon.sokol_default = true; // TODO: Don't hard code icon path, allow changing through options