diff --git a/engine/build.zig b/engine/build.zig index 59557bd..c5287af 100644 --- a/engine/build.zig +++ b/engine/build.zig @@ -53,33 +53,60 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void { } const engine_lib = b.createModule(.{ - .root_source_file = b.path("src/lib/root.zig") + .root_source_file = b.path("src/game_lib/root.zig") }); engine_lib.addOptions("build_options", build_options); - opts.root_module.addImport("engine", engine_lib); - const runtime_module = createRuntimeModule(b, .{ + const dep_stb = b.dependency("stb", .{}); + engine_lib.addImport("stb_image", dep_stb.module("stb_image")); + + opts.root_module.addImport("engine_lib", engine_lib); + + const engine_module = createEngineModule(b, .{ .target = opts.target, .optimize = opts.optimize, .has_tracy = has_tracy, .has_imgui = has_imgui }); - runtime_module.root_module.addImport("lib", engine_lib); + engine_module.root_module.addImport("engine_lib", engine_lib); if (statically_linked) { - runtime_module.root_module.addImport("game", opts.root_module); + engine_module.root_module.addImport("game", opts.root_module); + } + + const assset_bundler_tool = buildAssetBundler( + b, b.graph.host, .ReleaseSafe, + engine_module.root_module, + opts.root_module + ); + + const asset_bundler_step = b.addRunArtifact(assset_bundler_tool); + asset_bundler_step.addDirectoryArg(opts.asset_dir); + const assets_bundle_file = asset_bundler_step.addOutputFileArg("assets.bin"); + + const runtime_mod = b.createModule(.{ + .root_source_file = b.path("src/runtime/main.zig"), + .target = opts.target, + .optimize = opts.optimize, + }); + runtime_mod.addImport("engine", engine_module.root_module); + if (!load_assets_at_runtime) { + // TODO: Add compression on asset bundle + runtime_mod.addAnonymousImport("asset_bundle", .{ + .root_source_file = assets_bundle_file + }); } var run_cmd_step: *std.Build.Step = undefined; if (isWasm) { const build_step = buildWasm(b, .{ .name = "index", - .root_module = runtime_module.root_module, - .dep_sokol = runtime_module.dep_sokol, + .root_module = runtime_mod, + .dep_sokol = engine_module.dep_sokol, .outer_builder = outer_builder }); outer_builder.getInstallStep().dependOn(build_step); - const dep_sokol = runtime_module.dep_sokol; + const dep_sokol = engine_module.dep_sokol; const dep_emsdk = dep_sokol.builder.dependency("emsdk", .{}); const emrun_step = sokol.emRunStep(outer_builder, .{ .name = "index", .emsdk = dep_emsdk }); emrun_step.step.dependOn(build_step); @@ -87,10 +114,10 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void { // TODO: Create a zip archive of all of the files. Would be useful for easier itch.io upload } else { - runtime_module.root_module.link_libc = true; + runtime_mod.link_libc = true; const exe = buildNative(b, .{ .name = opts.name, - .root_module = runtime_module.root_module, + .root_module = runtime_mod, .win32_has_console = opts.win32_has_console, .win32_png_icon = opts.win32_png_icon }); @@ -123,9 +150,15 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void { const run_step = outer_builder.step("run", "Run game"); run_step.dependOn(run_cmd_step); + + { + const install_assets_bin = outer_builder.addInstallFileWithDir(assets_bundle_file, .bin, "assets.bin"); + const assets_step = outer_builder.step("assets", "Build assets file"); + assets_step.dependOn(&install_assets_bin.step); + } } -const RuntimeModule = struct { +const EngineModule = struct { root_module: *std.Build.Module, dep_sokol: *std.Build.Dependency, @@ -138,9 +171,9 @@ const RuntimeModule = struct { }; }; -fn createRuntimeModule(b: *std.Build, opts: RuntimeModule.Options) RuntimeModule { +fn createEngineModule(b: *std.Build, opts: EngineModule.Options) EngineModule { const mod = b.createModule(.{ - .root_source_file = b.path("src/runtime/main.zig"), + .root_source_file = b.path("src/engine/root.zig"), .target = opts.target, .optimize = opts.optimize, .link_libc = true @@ -199,7 +232,7 @@ fn createRuntimeModule(b: *std.Build, opts: RuntimeModule.Options) RuntimeModule mod.addIncludePath(dep_sokol_c.path("util")); mod.addCSourceFile(.{ - .file = b.path("src/runtime/fontstash/sokol_fontstash_impl.c"), + .file = b.path("src/engine/fontstash/sokol_fontstash_impl.c"), .flags = cflags.items }); } @@ -216,12 +249,36 @@ fn createRuntimeModule(b: *std.Build, opts: RuntimeModule.Options) RuntimeModule // // TODO: Define buid config for wasm // } - return RuntimeModule{ + return EngineModule{ .root_module = mod, .dep_sokol = dep_sokol }; } +fn buildAssetBundler( + b: *std.Build, + target: std.Build.ResolvedTarget, + optimize: std.builtin.OptimizeMode, + engine: *std.Build.Module, + game: *std.Build.Module +) *std.Build.Step.Compile { + const mod = b.createModule(.{ + .target = target, + .optimize = optimize, + .root_source_file = b.path("tools/asset-bundler.zig"), + }); + mod.addImport("engine", engine); + mod.addImport("game", game); + // mod.addImport("asset_bundle", b.createModule(.{ + // .root_source_file = b.path("src/runtime/asset_bundle.zig") + // })); + + return b.addExecutable(.{ + .name = "asset-bundler", + .root_module = mod, + }); +} + fn buildPngToIconTool( b: *std.Build, target: std.Build.ResolvedTarget, @@ -310,7 +367,7 @@ fn buildWasm(b: *std.Build, opts: BuildWasmOptions) *std.Build.Step { .use_webgl2 = true, .use_emmalloc = true, .use_filesystem = false, - .shell_file_path = b.path("src/runtime/shell.html"), + .shell_file_path = b.path("src/engine/shell.html"), }) catch unreachable; return &link_step.step; diff --git a/engine/src/engine/asset_bundle.zig b/engine/src/engine/asset_bundle.zig new file mode 100644 index 0000000..9ae2872 --- /dev/null +++ b/engine/src/engine/asset_bundle.zig @@ -0,0 +1,166 @@ +const std = @import("std"); +const Lib = @import("engine_lib"); + +const AssetBundle = @This(); + +fonts: std.ArrayList(Font), +sounds: std.ArrayList(Sound), +textures: std.ArrayList(Texture), +data: std.ArrayList(u8), + +const DataSegment = extern struct { + offset: u64, + len: u64, +}; + +const Font = extern struct { + asset_id: Lib.Asset.Id, + data: DataSegment +}; + +const Sound = extern struct { + asset_id: Lib.Asset.Id, + data: DataSegment +}; + +const Texture = extern struct { + asset_id: Lib.Asset.Id, + info: Lib.Texture.Info, + data: DataSegment +}; + +const Header = extern struct { + magic: Magic, + fonts_len: u32, + sounds_len: u32, + textures_len: u32, + data_len: u32, + + pub const Magic = enum(u32) { + v1 = 0x961C8C65, + _, + }; +}; + +const endianness: std.builtin.Endian = .little; + +pub fn init() AssetBundle { + return AssetBundle{ + .fonts = .empty, + .sounds = .empty, + .textures = .empty, + .data = .empty, + }; +} + +pub fn deinit(self: *AssetBundle, gpa: std.mem.Allocator) void { + self.sounds.deinit(gpa); + self.textures.deinit(gpa); + self.fonts.deinit(gpa); + self.data.deinit(gpa); +} + +pub fn getData(self: AssetBundle, data: DataSegment) []const u8 { + return self.data.items[data.offset..][0..data.len]; +} + +fn insertData( + self: *AssetBundle, + gpa: std.mem.Allocator, + data: []const u8 +) !DataSegment { + const data_offset = self.data.items.len; + try self.data.appendSlice(gpa, data); + + return DataSegment{ + .offset = data_offset, + .len = data.len + }; +} + +pub fn appendSound( + self: *AssetBundle, + gpa: std.mem.Allocator, + id: Lib.Asset.Id, + data: []const u8 +) !void { + try self.sounds.append(gpa, Sound{ + .asset_id = id, + .data = try self.insertData(gpa, data) + }); +} + +pub fn appendFont( + self: *AssetBundle, + gpa: std.mem.Allocator, + id: Lib.Asset.Id, + data: []const u8 +) !void { + try self.fonts.append(gpa, Font{ + .asset_id = id, + .data = try self.insertData(gpa, data) + }); +} + +pub fn appendTexture( + self: *AssetBundle, + gpa: std.mem.Allocator, + id: Lib.Asset.Id, + data: []const u8, + info: Lib.Texture.Info +) !void { + try self.textures.append(gpa, Texture{ + .asset_id = id, + .info = info, + .data = try self.insertData(gpa, data) + }); +} + +pub fn write(self: AssetBundle, writer: *std.Io.Writer) !void { + try writer.writeStruct(Header{ + .magic = .v1, + .fonts_len = @intCast(self.fonts.items.len), + .sounds_len = @intCast(self.sounds.items.len), + .textures_len = @intCast(self.textures.items.len), + .data_len = @intCast(self.data.items.len) + }, endianness); + + try writer.writeSliceEndian(Font, self.fonts.items, endianness); + try writer.writeSliceEndian(Sound, self.sounds.items, endianness); + try writer.writeSliceEndian(Texture, self.textures.items, endianness); + try writer.writeAll(self.data.items); +} + +pub fn read(self: *AssetBundle, gpa: std.mem.Allocator, reader: *std.Io.Reader) !void { + const header = try reader.takeStruct(Header, endianness); + if (header.magic != .v1) { + return error.InvalidMagic; + } + + try self.fonts.ensureTotalCapacity(gpa, header.fonts_len); + try self.sounds.ensureTotalCapacity(gpa, header.sounds_len); + try self.textures.ensureTotalCapacity(gpa, header.textures_len); + try self.data.ensureTotalCapacity(gpa, header.data_len); + + self.fonts.clearRetainingCapacity(); + self.sounds.clearRetainingCapacity(); + self.textures.clearRetainingCapacity(); + self.data.clearRetainingCapacity(); + + try reader.readSliceEndian( + Font, + self.fonts.addManyAsSliceAssumeCapacity(header.fonts_len), + endianness + ); + try reader.readSliceEndian( + Sound, + self.sounds.addManyAsSliceAssumeCapacity(header.sounds_len), + endianness + ); + try reader.readSliceEndian( + Texture, + self.textures.addManyAsSliceAssumeCapacity(header.textures_len), + endianness + ); + try reader.readSliceAll(self.data.addManyAsSliceAssumeCapacity(header.data_len)); +} diff --git a/engine/src/runtime/audio.zig b/engine/src/engine/audio.zig similarity index 96% rename from engine/src/runtime/audio.zig rename to engine/src/engine/audio.zig index 237c9c2..cc6f0c5 100644 --- a/engine/src/runtime/audio.zig +++ b/engine/src/engine/audio.zig @@ -4,7 +4,7 @@ const assert = std.debug.assert; const tracy = @import("tracy"); -const Lib = @import("lib"); +const Lib = @import("engine_lib"); const Math = Lib.Math; const STBVorbis = @import("stb_vorbis"); @@ -215,7 +215,7 @@ const ThreadState = struct { temp_channel_buffer: []f32, pub const SoundInstance = struct { - sound_id: Lib.SoundId, + sound_id: Lib.Sound.Id, volume: f32 = 0, cursor: u32 = 0, }; @@ -257,7 +257,7 @@ const SokolSetupOptions = struct { const SoundList = Lib.GenerationalArrayList(u8, u8, Sound); comptime { - assert(@bitSizeOf(SoundList.Id) == @bitSizeOf(Lib.SoundId)); + assert(@bitSizeOf(SoundList.Id) == @bitSizeOf(Lib.Sound.Id)); } gpa: std.mem.Allocator, @@ -353,7 +353,7 @@ pub fn deinit(self: *AudioSystem) void { self.sounds.deinit(self.gpa); } -pub fn addSound(self: *AudioSystem) !Lib.SoundId { +pub fn addSound(self: *AudioSystem) !Lib.Sound.Id { self.sounds_mutex.lock(); defer self.sounds_mutex.unlock(); @@ -363,7 +363,7 @@ pub fn addSound(self: *AudioSystem) !Lib.SoundId { return @enumFromInt(sound_id.asInt()); } -pub fn removeSound(self: *AudioSystem, id: Lib.SoundId) void { +pub fn removeSound(self: *AudioSystem, id: Lib.Sound.Id) void { self.sounds_mutex.lock(); defer self.sounds_mutex.unlock(); @@ -388,7 +388,7 @@ pub const DataOptions = struct { playback_style: ?PlaybackStyle = null, }; -pub fn setSoundData(self: *AudioSystem, id: Lib.SoundId, opts: DataOptions) !void { +pub fn setSoundData(self: *AudioSystem, id: Lib.Sound.Id, opts: DataOptions) !void { const sound = self.sounds.get(.fromInt(@intFromEnum(id))) orelse return error.SoundNotFound; assert(opts.format == .vorbis); @@ -441,10 +441,6 @@ pub fn setSoundData(self: *AudioSystem, id: Lib.SoundId, opts: DataOptions) !voi } } -pub fn get(self: *AudioSystem, id: Lib.SoundId) Sound { - return self.sounds.items[@intFromEnum(id)]; -} - fn sokolStream(self: *AudioSystem, buffer: [*c]f32, num_frames: u32, num_channels: u32) void { if (!self.running.load(.seq_cst)) { return; @@ -482,7 +478,14 @@ fn sokolStream(self: *AudioSystem, buffer: [*c]f32, num_frames: u32, num_channel @memset(buffer[0..@intCast(num_frames * num_channels)], 0); for (thread_state.instances.items) |*instance| { - const sound = self.sounds.get(.fromInt(@intFromEnum(instance.sound_id))) orelse continue; + const sound = self.sounds.get(.fromInt(@intFromEnum(instance.sound_id))) orelse { + log.warn("Attempt to play that doesn't exist", .{}); + continue; + }; + if (sound.* == .nil) { + log.warn("Attempt to play .nil sound", .{}); + continue; + } assert(sound.getSampleRate() == sample_rate); // TODO: const sample_count = sound.streamChannels(.{ diff --git a/engine/src/runtime/fontstash/context.zig b/engine/src/engine/fontstash/context.zig similarity index 100% rename from engine/src/runtime/fontstash/context.zig rename to engine/src/engine/fontstash/context.zig diff --git a/engine/src/runtime/fontstash/font.zig b/engine/src/engine/fontstash/font.zig similarity index 100% rename from engine/src/runtime/fontstash/font.zig rename to engine/src/engine/fontstash/font.zig diff --git a/engine/src/runtime/fontstash/root.zig b/engine/src/engine/fontstash/root.zig similarity index 100% rename from engine/src/runtime/fontstash/root.zig rename to engine/src/engine/fontstash/root.zig diff --git a/engine/src/runtime/fontstash/sokol_fontstash_impl.c b/engine/src/engine/fontstash/sokol_fontstash_impl.c similarity index 100% rename from engine/src/runtime/fontstash/sokol_fontstash_impl.c rename to engine/src/engine/fontstash/sokol_fontstash_impl.c diff --git a/engine/src/runtime/graphics.zig b/engine/src/engine/graphics.zig similarity index 80% rename from engine/src/runtime/graphics.zig rename to engine/src/engine/graphics.zig index 6edec87..b3ea0b6 100644 --- a/engine/src/runtime/graphics.zig +++ b/engine/src/engine/graphics.zig @@ -6,7 +6,7 @@ const sapp = sokol.app; const simgui = sokol.imgui; const sgl = sokol.gl; -const Lib = @import("lib"); +const Lib = @import("engine_lib"); const Math = Lib.Math; const Vec2 = Math.Vec2; const Vec4 = Math.Vec4; @@ -57,8 +57,13 @@ const Texture = struct { const Data = struct { width: u32, height: u32, - rgba: [*]u8 + rgba: [*]u8, }; + + fn destroy(self: *Texture) void { + sg.destroyView(self.view); + sg.destroyImage(self.image); + } }; const Font = struct { @@ -74,11 +79,19 @@ const Font = struct { const FontArray = Lib.GenerationalArrayList(u8, u8, Font); comptime { - assert(@bitSizeOf(FontArray.Id) == @bitSizeOf(Lib.FontId)); + assert(@bitSizeOf(FontArray.Id) == @bitSizeOf(Lib.Font.Id)); + assert(FontArray.Id.none.asInt() == @intFromEnum(Lib.Font.Id.nil)); +} + +const TexturesArray = Lib.GenerationalArrayList(u8, u8, Texture); +comptime { + // TODO: This is PITA, can this be refactored? + assert(@bitSizeOf(TexturesArray.Id) == @bitSizeOf(Lib.Texture.Id)); + assert(TexturesArray.Id.none.asInt() == @intFromEnum(Lib.Texture.Id.nil)); } gpa: std.mem.Allocator, -textures: std.ArrayList(Texture) = .empty, +textures: TexturesArray = .empty, fonts: FontArray = .empty, scale_stack_buffer: [32]Vec2 = undefined, @@ -188,20 +201,24 @@ pub fn setupSokol(self: *Graphics) !void { } pub fn deinit(self: *Graphics, gpa: std.mem.Allocator) void { - if (self.sokol_state) |*sokol_state| { - imgui.shutdown(); - sokol_state.font_context.deinit(); - sgl.shutdown(); - sg.shutdown(); - } - var fonts_iter = self.fonts.iterator(); while (fonts_iter.nextItem()) |font| { font.deinit(self.gpa); } self.fonts.deinit(gpa); + var textures_iter = self.textures.iterator(); + while (textures_iter.nextItem()) |texture| { + texture.destroy(); + } self.textures.deinit(gpa); + + if (self.sokol_state) |*sokol_state| { + imgui.shutdown(); + sokol_state.font_context.deinit(); + sgl.shutdown(); + sg.shutdown(); + } } pub fn drawCommand(self: *Graphics, command: Command) void { @@ -293,17 +310,6 @@ pub fn drawCommands(self: *Graphics, commands: []const Command) void { } } -pub fn refreshFonts(self: *Graphics) !void { - const sokol_state = self.sokol_state orelse return; - - var fonts_iter = self.fonts.iterator(); - while (fonts_iter.nextItem()) |font| { - if (font.fontstash_id == null and font.data != null) { - font.fontstash_id = try sokol_state.font_context.addFont("", font.data.?); - } - } -} - pub fn beginFrame(self: *Graphics) !void { assert(self.sokol_state != null); const sokol_state = self.sokol_state.?; @@ -378,16 +384,24 @@ const Vertex = struct { uv: Vec2 }; -fn drawQuad(self: *Graphics, quad: [4]Vertex, color: Vec4, texture_id: Lib.TextureId) void { +fn drawQuad(self: *Graphics, quad: [4]Vertex, color: Vec4, texture_id: Lib.Texture.Id) void { assert(self.sokol_state != null); const sokol_state = self.sokol_state.?; + var view: ?sg.View = null; + if (self.textures.get(.fromInt(@intFromEnum(texture_id)))) |texture| { + view = texture.view; + } else { + log.warn("Attempt to use texture that doesn't exist, id={}", .{texture_id}); + } + sgl.enableTexture(); defer sgl.disableTexture(); - const view = self.textures.items[@intFromEnum(texture_id)].view; // TODO: Make sampler configurable - sgl.texture(view, sokol_state.nearest_sampler); + if (view != null) { + sgl.texture(view.?, sokol_state.nearest_sampler); + } sgl.beginQuads(); defer sgl.end(); @@ -465,17 +479,9 @@ fn drawLine(from: Vec2, to: Vec2, color: Vec4, width: f32) void { ); } -fn makeView(image: sg.Image) !sg.View { - const image_view = sg.makeView(.{ - .texture = .{ .image = image } - }); - if (image_view.id == sg.invalid_id) { - return error.InvalidView; - } - return image_view; -} +// ---------------------- Textures ----------------- // -fn makeImageWithMipMaps(mipmaps: []const Texture.Data) !sg.Image { +fn initImageWithMipMaps(image: sg.Image, mipmaps: []const Texture.Data) !void { if (mipmaps.len == 0) { return error.NoMipMaps; } @@ -490,7 +496,7 @@ fn makeImageWithMipMaps(mipmaps: []const Texture.Data) !sg.Image { }); } - const image = sg.makeImage(.{ + sg.initImage(image, .{ .width = @intCast(mipmaps[0].width), .height = @intCast(mipmaps[0].height), .pixel_format = .RGBA8, @@ -500,35 +506,60 @@ fn makeImageWithMipMaps(mipmaps: []const Texture.Data) !sg.Image { .num_mipmaps = @intCast(mip_levels.items.len), .data = data }); - if (image.id == sg.invalid_id) { - return error.InvalidImage; + if (sg.queryImageState(image) != .VALID) { + return error.InitImage; + } +} + +pub fn addTexture(self: *Graphics) !Lib.Texture.Id { + const texture_id = try self.textures.insert(self.gpa, .{ + .image = .{ .id = sg.invalid_id }, + .view = .{ .id = sg.invalid_id } + }); + return @enumFromInt(texture_id.asInt()); +} + +pub fn removeTexture(self: *Graphics, id: Lib.Texture.Id) void { + _ = self.textures.remove(id); +} + +pub fn setTextureData(self: *Graphics, id: Lib.Texture.Id, data: Texture.Data) !void { + if (self.sokol_state == null) { + return error.SokolNotSetup; } - return image; -} + const texture = self.textures.get(.fromInt(@intFromEnum(id))) orelse return; -pub fn loadTexture(self: *Graphics, gpa: std.mem.Allocator, mipmaps: []const Texture.Data) !Lib.TextureId { - const image = try makeImageWithMipMaps(mipmaps); - errdefer sg.deallocImage(image); - - const view = try makeView(image); - errdefer sg.deallocView(view); - - assert(mipmaps.len > 0); - const index = self.textures.items.len; - try self.textures.append(gpa, .{ - .image = image, - .view = view, - .info = .{ - .width = mipmaps[0].width, - .height = mipmaps[0].height, + if (texture.image.id == sg.invalid_id) { + texture.image = sg.allocImage(); + if (texture.image.id == sg.invalid_id) { + return error.AllocImage; } - }); + } - return @enumFromInt(index); + if (texture.view.id == sg.invalid_id) { + texture.view = sg.allocView(); + if (texture.view.id == sg.invalid_id) { + return error.AllocView; + } + } + + sg.uninitView(texture.view); + sg.uninitImage(texture.image); + + try initImageWithMipMaps(texture.image, &.{ data }); + + sg.initView(texture.view, .{ + .texture = .{ .image = texture.image } + }); + if (sg.queryViewState(texture.view) != .VALID) { + return error.InitView; + } } -pub fn addFont(self: *Graphics) !Lib.FontId { +// ---------------------- Fonts ----------------- // + +pub fn addFont(self: *Graphics) !Lib.Font.Id { const font_id = try self.fonts.insert(self.gpa, .{ .fontstash_id = null, .data = null @@ -536,18 +567,23 @@ pub fn addFont(self: *Graphics) !Lib.FontId { return @enumFromInt(font_id.asInt()); } -pub fn removeFont(self: *Graphics, id: Lib.FontId) void { +pub fn removeFont(self: *Graphics, id: Lib.Font.Id) void { _ = self.fonts.remove(.fromInt(@intFromEnum(id))); } -pub fn setFontData(self: *Graphics, id: Lib.FontId, data: []const u8) !void { - if (self.fonts.get(.fromInt(@intFromEnum(id)))) |font| { - const new_data = try self.gpa.dupe(u8, data); +pub fn setFontData(self: *Graphics, id: Lib.Font.Id, data: []const u8) !void { + const sokol_state = self.sokol_state orelse return error.SokolNotSetup; - if (font.data) |old_data| { - self.gpa.free(old_data); - } - font.data = new_data; - font.fontstash_id = null; + const font = self.fonts.get(.fromInt(@intFromEnum(id))) orelse return; + + const new_data = try self.gpa.dupe(u8, data); + errdefer self.gpa.free(new_data); + + const new_fontstash_id = try sokol_state.font_context.addFont("", new_data); + + if (font.data) |old_data| { + self.gpa.free(old_data); } + font.data = new_data; + font.fontstash_id = new_fontstash_id; } diff --git a/engine/src/runtime/imgui.zig b/engine/src/engine/imgui.zig similarity index 99% rename from engine/src/runtime/imgui.zig rename to engine/src/engine/imgui.zig index 42adb31..de6cc77 100644 --- a/engine/src/runtime/imgui.zig +++ b/engine/src/engine/imgui.zig @@ -9,7 +9,7 @@ const sokol = @import("sokol"); const sapp = sokol.app; const simgui = sokol.imgui; -const Lib = @import("lib"); +const Lib = @import("engine_lib"); const build_options = Lib.build_options; const enabled = build_options.has_imgui; diff --git a/engine/src/runtime/input.zig b/engine/src/engine/input.zig similarity index 99% rename from engine/src/runtime/input.zig rename to engine/src/engine/input.zig index 4e14208..bf43a88 100644 --- a/engine/src/runtime/input.zig +++ b/engine/src/engine/input.zig @@ -2,7 +2,7 @@ const std = @import("std"); const assert = std.debug.assert; const sokol = @import("sokol"); -const lib = @import("lib"); +const lib = @import("engine_lib"); const Frame = lib.Frame; const Nanoseconds = lib.Nanoseconds; const Vec2 = lib.Math.Vec2; diff --git a/engine/src/runtime/engine.zig b/engine/src/engine/root.zig similarity index 75% rename from engine/src/runtime/engine.zig rename to engine/src/engine/root.zig index 6b65e32..1606c96 100644 --- a/engine/src/runtime/engine.zig +++ b/engine/src/engine/root.zig @@ -5,7 +5,7 @@ const assert = std.debug.assert; const sokol = @import("sokol"); const sapp = sokol.app; -pub const Input = @import("./input.zig"); +const Input = @import("./input.zig"); const ScreenScalar = @import("./screen_scaler.zig"); pub const imgui = @import("./imgui.zig"); @@ -17,7 +17,9 @@ const STBImage = @import("stb_image"); const Gfx = Graphics; -const Lib = @import("lib"); +pub const Lib = @import("engine_lib"); +pub const AssetBundle = @import("./asset_bundle.zig"); + const build_options = Lib.build_options; const debug_keybinds = (builtin.mode == .Debug); const GameCallbacks = Lib.Callbacks; @@ -343,21 +345,22 @@ const GameState = struct { } } - fn runGameInit(self: *GameState, init_fn: GameCallbacks.InitFn, initial: Init, assets: []const Lib.Asset) void { + fn runGameInit( + self: *GameState, + init_fn: GameCallbacks.InitFn, + initial: Init, + asset_registry: Lib.Asset.Registry + ) void { assert(self.frame == null); const opts = Lib.Init{ .gpa = self.gpa, .seed = initial.seed, - .assets = .{ - .items = assets - } + .assets = asset_registry }; self.state = init_fn(&opts); self.frame = .init(self.gpa, initial.screen_size); - self.frame.?.assets = .{ - .items = assets - }; + self.frame.?.assets = asset_registry; } fn runGameDeinit(self: *GameState, deinit_fn: GameCallbacks.DeinitFn) void { @@ -476,13 +479,157 @@ const Recording = struct { } }; +const AssetManager = struct { + const LoadAsset = union(enum) { + font: struct { + id: Lib.Font.Id, + path: []const u8 + }, + sound: struct { + id: Lib.Sound.Id, + path: []const u8 + }, + texture: struct { + id: Lib.Texture.Id, + path: []const u8 + }, + + pub fn clone(self: LoadAsset, arena: std.mem.Allocator) !LoadAsset { + return switch (self) { + .font => |opts| LoadAsset{ + .font = .{ + .id = opts.id, + .path = try arena.dupe(u8, opts.path) + } + }, + .sound => |opts| LoadAsset{ + .sound = .{ + .id = opts.id, + .path = try arena.dupe(u8, opts.path) + } + }, + .texture => |opts| LoadAsset{ + .texture = .{ + .id = opts.id, + .path = try arena.dupe(u8, opts.path) + } + } + }; + } + }; + + gpa: std.mem.Allocator, + assets: std.ArrayList(Lib.Asset), + textures: std.ArrayList(Lib.Texture), + + dir: ?std.fs.Dir, + queued_loads_arena: std.heap.ArenaAllocator, + queued_loads: std.ArrayList(LoadAsset), + + pub fn init(gpa: std.mem.Allocator, dir: ?std.fs.Dir) AssetManager { + return AssetManager{ + .gpa = gpa, + .assets = .empty, + .textures = .empty, + .dir = dir, + .queued_loads_arena = .init(gpa), + .queued_loads = .empty + }; + } + + pub fn deinit(self: *AssetManager) void { + self.queued_loads_arena.deinit(); + self.assets.deinit(self.gpa); + self.textures.deinit(self.gpa); + } + + pub fn initRegistry(self: AssetManager) Lib.Asset.Registry { + return Lib.Asset.Registry{ + .assets = self.assets.items, + .textures = self.textures.items + }; + } + + pub fn enqueueLoad(self: *AssetManager, load: LoadAsset) !void { + const arena = self.queued_loads_arena.allocator(); + + try self.queued_loads.append(arena, try load.clone(arena)); + } + + pub fn performQueuedLoads(self: *AssetManager, graphics: *Graphics, audio: *Audio) !void { + if (self.dir == null) { + if (self.queued_loads.items.len > 0) { + return error.MissingAssetDirectory; + } + return; + } + + const dir = self.dir.?; + for (self.queued_loads.items) |queued_load| { + switch (queued_load) { + .font => |opts| { + const font_data = try readFileAll(self.gpa, dir, opts.path); + defer self.gpa.free(font_data); + try graphics.setFontData(opts.id, font_data); + }, + .sound => |opts| { + const sound_data = try readFileAll(self.gpa, dir, opts.path); + defer self.gpa.free(sound_data); + try audio.setSoundData(opts.id, .{ + .data = sound_data, + .format = .vorbis + }); + }, + .texture => |opts| { + const image_data = try readFileAll(self.gpa, dir, opts.path); + defer self.gpa.free(image_data); + + const image = try STBImage.load(image_data); + defer image.deinit(); + + // TODO: Assert that loaded data matches precomputed texture info + try graphics.setTextureData(opts.id, .{ + .width = image.width, + .height = image.height, + .rgba = image.rgba8_pixels, + }); + } + } + } + + _ = self.queued_loads_arena.reset(.free_all); + self.queued_loads = .empty; + } + + fn hasTexture(self: AssetManager, texture_id: Lib.Texture.Id) bool { + for (self.textures.items) |texture| { + if (texture.id == texture_id) { + return true; + } + } + return false; + } + + pub fn assertAssetsLoaded(self: AssetManager) void { + for (self.assets.items) |asset| { + switch (asset.kind) { + .texture => |texture_id| { + assert(self.hasTexture(texture_id)); + }, + else => {} // TODO: + } + } + } +}; + const Engine = @This(); allocator: std.mem.Allocator, graphics: Graphics, audio: Audio, -assets: std.ArrayList(Lib.Asset), +asset_bundle: ?AssetBundle = null, +asset_manager: AssetManager, game_linking: GameLinking, game_state: GameState, @@ -501,6 +648,7 @@ recording_tick: u64, const RunOptions = struct { allocator: std.mem.Allocator, game_library_path: ?[]const u8 = null, + asset_bundle_bytes: ?[]const u8 = null, do_recording: bool = (builtin.mode == .Debug), screen_width: u32 = 640, screen_height: u32 = 480, @@ -514,11 +662,30 @@ pub fn run(self: *Engine, opts: RunOptions) !void { game_linking = try .initStatic(); } + var maybe_asset_bundle: ?AssetBundle = null; + if (opts.asset_bundle_bytes) |asset_bundle_bytes| { + var asset_bundle = AssetBundle.init(); + errdefer asset_bundle.deinit(opts.allocator); + + var reader = std.Io.Reader.fixed(asset_bundle_bytes); + asset_bundle.read(opts.allocator, &reader) catch |e| { + log.err("Failed to load asset bundle: {}", .{e}); + return error.FailedToLoadAssetBundle; + }; + + maybe_asset_bundle = asset_bundle; + } + const initial = GameState.Init{ .seed = @bitCast(std.time.milliTimestamp()), .screen_size = .initFromInt(u32, opts.screen_width, opts.screen_height) }; + var asset_dir: ?std.fs.Dir = null; + if (build_options.load_assets_at_runtime) { + asset_dir = try std.fs.cwd().openDir(build_options.asset_dir, .{ .iterate = true }); + } + self.* = Engine{ .allocator = opts.allocator, .graphics = try .init(.{ @@ -543,64 +710,100 @@ pub fn run(self: *Engine, opts: RunOptions) !void { .play_recording = false, .recording_tick = 0, .seed = initial.seed, - .assets = .empty + .asset_manager = .init(opts.allocator, asset_dir), + .asset_bundle = maybe_asset_bundle, }; if (build_options.load_assets_at_runtime) { - var dir = try std.fs.cwd().openDir(build_options.asset_dir, .{ .iterate = true }); - defer dir.close(); + const dir = asset_dir.?; + var loader: Lib.Asset.Loader = .init(opts.allocator, dir); defer loader.deinit(); game_linking.callbacks.loadAssets(&loader); for (loader.assets.items) |asset_desc| { switch (asset_desc.kind) { .font => |path| { - const f = try dir.openFile(path, .{}); - defer f.close(); - - var wa: std.Io.Writer.Allocating = .init(self.allocator); - defer wa.deinit(); - - var buffer: [4 * 4096]u8 = undefined; - var reader = f.reader(&buffer); - _ = try reader.interface.stream(&wa.writer, .unlimited); - const font_id = try self.graphics.addFont(); - try self.graphics.setFontData(font_id, wa.written()); - try self.assets.append(self.allocator, .{ + try self.asset_manager.assets.append(self.allocator, .{ .id = asset_desc.id, .kind = .{ .font = font_id } }); + + try self.asset_manager.enqueueLoad(.{ + .font = .{ + .id = font_id, + .path = path + } + }); }, - .texture => |path| { - _ = path; // autofix + .texture => |texture| { + const texture_id = try self.graphics.addTexture(); + try self.asset_manager.assets.append(self.allocator, .{ + .id = asset_desc.id, + .kind = .{ .texture = texture_id } + }); + + try self.asset_manager.enqueueLoad(.{ + .texture = .{ + .id = texture_id, + .path = texture.path + } + }); + try self.asset_manager.textures.append(self.asset_manager.gpa, .{ + .id = texture_id, + .info = texture.info + }); }, .sound => |path| { - const f = try dir.openFile(path, .{}); - defer f.close(); - - var wa: std.Io.Writer.Allocating = .init(self.allocator); - defer wa.deinit(); - - var buffer: [4 * 4096]u8 = undefined; - var reader = f.reader(&buffer); - _ = try reader.interface.stream(&wa.writer, .unlimited); - const sound_id = try self.audio.addSound(); - try self.audio.setSoundData(sound_id, .{ - .data = wa.written(), - .format = .vorbis - }); - try self.assets.append(self.allocator, .{ + try self.asset_manager.assets.append(self.allocator, .{ .id = asset_desc.id, .kind = .{ .sound = sound_id } }); + + try self.asset_manager.enqueueLoad(.{ + .sound = .{ + .id = sound_id, + .path = path + } + }); }, } } } - self.game_state.runGameInit(game_linking.callbacks.init, initial, self.assets.items); + if (maybe_asset_bundle) |asset_bundle| { + for (asset_bundle.fonts.items) |font| { + const font_id = try self.graphics.addFont(); + try self.asset_manager.assets.append(self.allocator, .{ + .id = font.asset_id, + .kind = .{ .font = font_id } + }); + } + + for (asset_bundle.textures.items) |texture| { + const texture_id = try self.graphics.addTexture(); + try self.asset_manager.assets.append(self.allocator, .{ + .id = texture.asset_id, + .kind = .{ .texture = texture_id } + }); + + try self.asset_manager.textures.append(self.asset_manager.gpa, .{ + .id = texture_id, + .info = texture.info + }); + } + + for (asset_bundle.sounds.items) |sound| { + const sound_id = try self.audio.addSound(); + try self.asset_manager.assets.append(self.allocator, .{ + .id = sound.asset_id, + .kind = .{ .sound = sound_id } + }); + } + } + + self.game_state.runGameInit(game_linking.callbacks.init, initial, self.asset_manager.initRegistry()); if (opts.do_recording) { self.recording = .init(opts.allocator, initial); @@ -660,6 +863,20 @@ pub fn run(self: *Engine, opts: RunOptions) !void { }); } +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(); + + var wa: std.Io.Writer.Allocating = .init(gpa); + defer wa.deinit(); + + var buffer: [4 * 4096]u8 = undefined; + var reader = f.reader(&buffer); + _ = try reader.interface.stream(&wa.writer, .unlimited); + + return try wa.toOwnedSlice(); +} + fn sokolInit(self: *Engine) !void { const zone = tracy.initZone(@src(), .{ }); defer zone.deinit(); @@ -667,7 +884,44 @@ fn sokolInit(self: *Engine) !void { try self.graphics.setupSokol(); self.audio.setupSokol(); - try self.graphics.refreshFonts(); + try self.asset_manager.performQueuedLoads(&self.graphics, &self.audio); + if (self.asset_bundle) |*asset_bundle| { + const registry = self.asset_manager.initRegistry(); + + for (asset_bundle.fonts.items) |font| { + const font_id = registry.getFont(font.asset_id); + assert(font_id != Lib.Font.Id.nil); + try self.graphics.setFontData(font_id, asset_bundle.getData(font.data)); + } + + for (asset_bundle.textures.items) |texture| { + const texture_id = registry.getTexture(texture.asset_id); + assert(texture_id != Lib.Texture.Id.nil); + + const image = try STBImage.load(asset_bundle.getData(texture.data)); + defer image.deinit(); + + try self.graphics.setTextureData(texture_id, .{ + .width = image.width, + .height = image.height, + .rgba = image.rgba8_pixels + }); + } + + for (asset_bundle.sounds.items) |sound| { + const sound_id = registry.getSound(sound.asset_id); + assert(sound_id != Lib.Sound.Id.nil); + try self.audio.setSoundData(sound_id, .{ + .data = asset_bundle.getData(sound.data), + .format = .vorbis + }); + } + + asset_bundle.deinit(self.allocator); + self.asset_bundle = null; + } + + self.asset_manager.assertAssetsLoaded(); } fn sokolCleanup(self: *Engine) void { @@ -684,7 +938,7 @@ fn sokolCleanup(self: *Engine) void { self.audio.deinit(); self.graphics.deinit(self.allocator); self.game_linking.deinit(self.allocator); - self.assets.deinit(self.allocator); + self.asset_manager.deinit(); } fn sokolFrame(self: *Engine) !void { @@ -759,7 +1013,7 @@ fn sokolFrame(self: *Engine) !void { self.game_state.deinit(); self.game_state = .init(self.allocator); - self.game_state.runGameInit(self.game_linking.callbacks.init, initial, self.assets.items); + self.game_state.runGameInit(self.game_linking.callbacks.init, initial, self.asset_manager.initRegistry()); if (self.recording) |recording| { recording.deinit(); @@ -811,7 +1065,7 @@ fn showDebugWindow(self: *Engine) !void { self.game_state.deinit(); self.game_state = .init(self.allocator); - self.game_state.runGameInit(self.game_linking.callbacks.init, recording.initial, self.assets.items); + self.game_state.runGameInit(self.game_linking.callbacks.init, recording.initial, self.asset_manager.initRegistry()); self.play_recording = true; self.recording_tick = 0; diff --git a/engine/src/runtime/screen_scaler.zig b/engine/src/engine/screen_scaler.zig similarity index 96% rename from engine/src/runtime/screen_scaler.zig rename to engine/src/engine/screen_scaler.zig index 3f0528b..8b802fa 100644 --- a/engine/src/runtime/screen_scaler.zig +++ b/engine/src/engine/screen_scaler.zig @@ -1,11 +1,11 @@ const Gfx = @import("./graphics.zig"); -const Math = @import("lib").Math; +const Math = @import("engine_lib").Math; const Vec2 = Math.Vec2; const Vec4 = Math.Vec4; const rgb = Math.rgb; -const Lib = @import("lib"); +const Lib = @import("engine_lib"); const Frame = Lib.Frame; const ScreenScalar = @This(); diff --git a/engine/src/runtime/shell.html b/engine/src/engine/shell.html similarity index 100% rename from engine/src/runtime/shell.html rename to engine/src/engine/shell.html diff --git a/engine/src/game_lib/asset.zig b/engine/src/game_lib/asset.zig new file mode 100644 index 0000000..80fd419 --- /dev/null +++ b/engine/src/game_lib/asset.zig @@ -0,0 +1,206 @@ +const std = @import("std"); +const log = std.log.scoped(.engine); + +const STBImage = @import("stb_image"); + +const Font = @import("./font.zig"); +const Sound = @import("./sound.zig"); +const Texture = @import("./texture.zig"); + +const Asset = @This(); + +id: Id, +kind: union(Type) { + font: Font.Id, + sound: Sound.Id, + texture: Texture.Id, +}, + +pub const Id = u32; +pub const Type = enum { + font, + sound, + texture +}; + +pub const Loader = struct { + const Descriptor = struct { + const KindTexture = struct { + path: []const u8, + info: Texture.Info + }; + + id: Id, + kind: union(Type) { + font: []const u8, + sound: []const u8, + texture: KindTexture + } + }; + + arena: std.heap.ArenaAllocator, + dir: std.fs.Dir, + assets: std.ArrayList(Descriptor), + add_failed: bool, + + pub fn init(gpa: std.mem.Allocator, dir: std.fs.Dir) Loader { + return Loader{ + .arena = .init(gpa), + .dir = dir, + .assets = .empty, + .add_failed = false + }; + } + + pub fn deinit(self: *Loader) void { + self.arena.deinit(); + self.* = undefined; + } + + fn readFile(self: *Loader, gpa: std.mem.Allocator, path: []const u8) ![]u8 { + const f = try self.dir.openFile(path, .{}); + defer f.close(); + + var wa: std.Io.Writer.Allocating = .init(gpa); + defer wa.deinit(); + + var buffer: [4 * 4096]u8 = undefined; + var reader = f.reader(&buffer); + _ = try reader.interface.stream(&wa.writer, .unlimited); + + return try wa.toOwnedSlice(); + } + + pub fn addFont(self: *Loader, id: Asset.Id, path: []const u8) void { + const allocator = self.arena.allocator(); + const path_dupe = allocator.dupe(u8, path) catch |e| { + log.err("Failed to duplicate path: {}", .{e}); + return; + }; + + self.assets.append(allocator, Descriptor{ + .id = id, + .kind = .{ .font = path_dupe } + }) catch |e| { + log.err("Failed add sound: {}", .{e}); + }; + } + + pub fn addSound(self: *Loader, id: Asset.Id, path: []const u8) void { + const allocator = self.arena.allocator(); + const path_dupe = allocator.dupe(u8, path) catch |e| { + log.err("Failed to duplicate path: {}", .{e}); + return; + }; + + self.assets.append(allocator, Descriptor{ + .id = id, + .kind = .{ .sound = path_dupe } + }) catch |e| { + log.err("Failed add sound: {}", .{e}); + }; + } + + fn addTextureFallible(self: *Loader, id: Asset.Id, path: []const u8) !void { + const allocator = self.arena.allocator(); + + var image_info: Texture.Info = undefined; + { + const file_contents = try self.readFile(allocator, path); + defer allocator.free(file_contents); + + const image = try STBImage.load(file_contents); + defer image.deinit(); + + image_info = .{ + .width = image.width, + .height = image.height, + }; + } + + const path_dupe = allocator.dupe(u8, path) catch |e| { + log.err("Failed to duplicate path: {}", .{e}); + return; + }; + + const texture = Descriptor.KindTexture{ + .path = path_dupe, + .info = image_info + }; + + self.assets.append(allocator, Descriptor{ + .id = id, + .kind = .{ .texture = texture } + }) catch |e| { + log.err("Failed add sound: {}", .{e}); + }; + } + + pub fn addTexture(self: *Loader, id: Asset.Id, path: []const u8) void { + self.addTextureFallible(id, path) catch |e| { + log.err("Failed to add texture: {}", .{e}); + self.add_failed = true; + }; + } +}; + +pub const Registry = struct { + assets: []const Asset, + textures: []const Texture, + + pub const empty = Registry{ + .assets = &.{}, + .textures = &.{} + }; + + pub fn getAsset(self: Registry, id: Asset.Id) ?Asset { + // TODO: Make this O(1)? + for (self.assets) |asset| { + if (asset.id == id) { + return asset; + } + } + return null; + } + + pub fn getSound(self: Registry, id: Asset.Id) Sound.Id { + const asset = self.getAsset(id) orelse return Sound.Id.nil; + if (asset.kind != .sound) { + return Sound.Id.nil; + } + return asset.kind.sound; + } + + pub fn getFont(self: Registry, id: Asset.Id) Font.Id { + const asset = self.getAsset(id) orelse return Font.Id.nil; + if (asset.kind != .font) { + return Font.Id.nil; + } + return asset.kind.font; + } + + pub fn getTexture(self: Registry, id: Asset.Id) Texture.Id { + const asset = self.getAsset(id) orelse return Texture.Id.nil; + if (asset.kind != .texture) { + return Texture.Id.nil; + } + return asset.kind.texture; + } + + pub fn getTextureInfo(self: Registry, id: Texture.Id) Texture.Info { + if (id == .nil) { + log.warn("Failed to access .nil texture info", .{}); + return .nil; + } + + // TODO: Make this O(1)? + for (self.textures) |texture| { + if (texture.id == id) { + return texture.info; + } + } + + log.warn("Failed to find texture info, id={}", .{id}); + return .nil; + } +}; diff --git a/engine/src/game_lib/font.zig b/engine/src/game_lib/font.zig new file mode 100644 index 0000000..6a2d846 --- /dev/null +++ b/engine/src/game_lib/font.zig @@ -0,0 +1,9 @@ +const std = @import("std"); + +id: Id, + +pub const Id = enum(u16) { + _, + + pub const nil: Id = @enumFromInt(std.math.maxInt(@typeInfo(Id).@"enum".tag_type)); +}; diff --git a/engine/src/lib/generational_array_list.zig b/engine/src/game_lib/generational_array_list.zig similarity index 99% rename from engine/src/lib/generational_array_list.zig rename to engine/src/game_lib/generational_array_list.zig index c37db12..964f581 100644 --- a/engine/src/lib/generational_array_list.zig +++ b/engine/src/game_lib/generational_array_list.zig @@ -6,8 +6,9 @@ const Allocator = std.mem.Allocator; /// This array provides: /// - O(1) insertion -/// - O(1) deletion /// - Unique IDs for every inserted item (assuming that generation doesn't overflow) +/// TODO: Use a stack of unused indexes to implement O(1) insertion +/// TODO: Implement a way to use a list *assumeCapacity() style of functions. pub fn GenerationalArrayList( Index: type, Generation: type, diff --git a/engine/src/lib/imgui.zig b/engine/src/game_lib/imgui.zig similarity index 100% rename from engine/src/lib/imgui.zig rename to engine/src/game_lib/imgui.zig diff --git a/engine/src/lib/math.zig b/engine/src/game_lib/math.zig similarity index 99% rename from engine/src/lib/math.zig rename to engine/src/game_lib/math.zig index 95d0178..dac6a33 100644 --- a/engine/src/lib/math.zig +++ b/engine/src/game_lib/math.zig @@ -367,6 +367,11 @@ pub const Rect = struct { .size = Vec2.zero }; + pub const unit = Rect{ + .pos = Vec2.zero, + .size = Vec2.init(1, 1) + }; + pub fn init(x: f32, y: f32, width: f32, height: f32) Rect { return Rect{ .pos = Vec2.init(x, y), diff --git a/engine/src/lib/root.zig b/engine/src/game_lib/root.zig similarity index 77% rename from engine/src/lib/root.zig rename to engine/src/game_lib/root.zig index eff0d3a..875486a 100644 --- a/engine/src/lib/root.zig +++ b/engine/src/game_lib/root.zig @@ -5,6 +5,7 @@ pub const build_options = @import("build_options"); pub const ImGui = @import("./imgui.zig"); pub const Math = @import("./math.zig"); pub const GenerationalArrayList = @import("./generational_array_list.zig").GenerationalArrayList; +const STBImage = @import("stb_image"); const rgb = Math.rgb; const Rect = Math.Rect; @@ -142,24 +143,13 @@ pub const MouseButton = enum { middle, }; -pub const TextureId = enum(u16) { - _, -}; - -pub const FontId = enum(u16) { - _, - - pub const nil: FontId = @enumFromInt(std.math.maxInt(@typeInfo(FontId).@"enum".tag_type)); -}; - -pub const SoundId = enum(u16) { - _, - - pub const nil: SoundId = @enumFromInt(std.math.maxInt(@typeInfo(SoundId).@"enum".tag_type)); -}; +pub const Texture = @import("./texture.zig"); +pub const Font = @import("./font.zig"); +pub const Sound = @import("./sound.zig"); +pub const Asset = @import("./asset.zig"); pub const Sprite = struct { - texture: TextureId, + texture: Texture.Id, uv: Rect }; @@ -221,7 +211,7 @@ pub const GraphicsCommand = union(enum) { pub const DrawText = struct { pos: Vec2, text: []const u8, - font: FontId, + font: Font.Id, size: f32, color: Vec4, }; @@ -240,7 +230,7 @@ pub const GraphicsCommand = union(enum) { pub const AudioCommand = union(enum) { pub const Play = struct { - id: SoundId, + id: Sound.Id, volume: f32 = 1 }; @@ -412,7 +402,7 @@ pub const Frame = struct { } pub const DrawTextOptions = struct { - font: FontId, + font: Font.Id, size: f32 = 16, color: Vec4 = rgb(255, 255, 255), }; @@ -491,121 +481,6 @@ pub const Init = struct { assets: Asset.Registry, }; -pub const Asset = struct { - id: Id, - kind: union(Type) { - font: FontId, - sound: SoundId, - texture: TextureId, - }, - - pub const Id = u32; - pub const Type = enum { - font, - sound, - texture - }; - - pub const Loader = struct { - const Descriptor = struct { - id: Id, - kind: union(Type) { - font: []const u8, - sound: []const u8, - texture: []const u8, - } - }; - - arena: std.heap.ArenaAllocator, - dir: std.fs.Dir, - assets: std.ArrayList(Descriptor), - - pub fn init(gpa: std.mem.Allocator, dir: std.fs.Dir) Loader { - return Loader{ - .arena = .init(gpa), - .dir = dir, - .assets = .empty, - }; - } - - pub fn deinit(self: *Loader) void { - self.arena.deinit(); - self.* = undefined; - } - - pub fn addFont(self: *Loader, id: Asset.Id, path: []const u8) void { - const allocator = self.arena.allocator(); - const path_dupe = allocator.dupe(u8, path) catch |e| { - log.err("Failed to duplicate path: {}", .{e}); - return; - }; - - self.assets.append(allocator, Descriptor{ - .id = id, - .kind = .{ .font = path_dupe } - }) catch |e| { - log.err("Failed add sound: {}", .{e}); - }; - } - - pub fn addSound(self: *Loader, id: Asset.Id, path: []const u8) void { - const allocator = self.arena.allocator(); - const path_dupe = allocator.dupe(u8, path) catch |e| { - log.err("Failed to duplicate path: {}", .{e}); - return; - }; - - self.assets.append(allocator, Descriptor{ - .id = id, - .kind = .{ .sound = path_dupe } - }) catch |e| { - log.err("Failed add sound: {}", .{e}); - }; - } - }; - - pub const Registry = struct { - items: []const Asset, - - pub const empty = Registry{ - .items = &.{} - }; - - pub fn get(self: Registry, id: Id) ?Asset { - for (self.items) |asset| { - if (asset.id == id) { - return asset; - } - } - return null; - } - - pub fn getSound(self: Registry, id: Id) SoundId { - const asset = self.get(id) orelse return SoundId.nil; - if (asset.kind != Type.sound) { - return SoundId.nil; - } - return asset.kind.sound; - } - - pub fn getFont(self: Registry, id: Id) FontId { - const asset = self.get(id) orelse return FontId.nil; - if (asset.kind != Type.font) { - return FontId.nil; - } - return asset.kind.font; - } - - pub fn getTexture(self: Registry, id: Id) TextureId { - const asset = self.get(id) orelse return TextureId.nil; - if (asset.kind != Type.font) { - return TextureId.nil; - } - return asset.kind.font; - } - }; -}; - pub const Callbacks = struct { pub const State = *anyopaque; diff --git a/engine/src/game_lib/sound.zig b/engine/src/game_lib/sound.zig new file mode 100644 index 0000000..6a2d846 --- /dev/null +++ b/engine/src/game_lib/sound.zig @@ -0,0 +1,9 @@ +const std = @import("std"); + +id: Id, + +pub const Id = enum(u16) { + _, + + pub const nil: Id = @enumFromInt(std.math.maxInt(@typeInfo(Id).@"enum".tag_type)); +}; diff --git a/engine/src/game_lib/texture.zig b/engine/src/game_lib/texture.zig new file mode 100644 index 0000000..661ee7d --- /dev/null +++ b/engine/src/game_lib/texture.zig @@ -0,0 +1,20 @@ +const std = @import("std"); + +id: Id, +info: Info, + +pub const Info = extern struct { + width: u32, + height: u32, + + pub const nil = Info{ + .width = 0, + .height = 0 + }; +}; + +pub const Id = enum(u16) { + _, + + pub const nil: Id = @enumFromInt(std.math.maxInt(@typeInfo(Id).@"enum".tag_type)); +}; diff --git a/engine/src/runtime/main.zig b/engine/src/runtime/main.zig index 99a5876..5322095 100644 --- a/engine/src/runtime/main.zig +++ b/engine/src/runtime/main.zig @@ -1,6 +1,7 @@ const std = @import("std"); const builtin = @import("builtin"); -const Engine = @import("./engine.zig"); +const Engine = @import("engine"); +const build_options = Engine.Lib.build_options; var engine: Engine = undefined; @@ -8,6 +9,11 @@ pub fn main() !void { var debug_allocator: std.heap.DebugAllocator(.{}) = .init; defer _ = debug_allocator.deinit(); + var asset_bundle_bytes: ?[]const u8 = null; + if (!build_options.load_assets_at_runtime) { + asset_bundle_bytes = @embedFile("asset_bundle"); + } + // TODO: Use tracy TracingAllocator var allocator: std.mem.Allocator = undefined; if (builtin.cpu.arch.isWasm()) { @@ -28,7 +34,8 @@ pub fn main() !void { try engine.run(.{ .allocator = allocator, - .game_library_path = game_library_path + .game_library_path = game_library_path, + .asset_bundle_bytes = asset_bundle_bytes }); } diff --git a/engine/tools/asset-bundler.zig b/engine/tools/asset-bundler.zig new file mode 100644 index 0000000..820732d --- /dev/null +++ b/engine/tools/asset-bundler.zig @@ -0,0 +1,79 @@ +const std = @import("std"); +const Game = @import("game"); +const Engine = @import("engine"); +const Lib = Engine.Lib; +const AssetBundle = Engine.AssetBundle; + +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(); + + var wa: std.Io.Writer.Allocating = .init(gpa); + defer wa.deinit(); + + var buffer: [4 * 4096]u8 = undefined; + var reader = f.reader(&buffer); + _ = try reader.interface.stream(&wa.writer, .unlimited); + + return try wa.toOwnedSlice(); +} + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + defer _ = gpa.deinit(); + + const args = try std.process.argsAlloc(allocator); + defer std.process.argsFree(allocator, args); + + if (args.len != 3) { + std.debug.print("Usage: ./asset-bundler ", .{}); + std.process.exit(1); + } + + const cwd = std.fs.cwd(); + const asset_dir_path = args[1]; + const output_path = args[2]; + + const asset_dir = try cwd.openDir(asset_dir_path, .{ .iterate = true }); + + var loader: Lib.Asset.Loader = .init(allocator, asset_dir); + defer loader.deinit(); + Game.loadAssets(&loader); + + var bundle = AssetBundle.init(); + defer bundle.deinit(allocator); + + for (loader.assets.items) |asset_desc| { + const id = asset_desc.id; + switch (asset_desc.kind) { + .font => |path| { + const font_data = try readFileAll(allocator, asset_dir, path); + defer allocator.free(font_data); + + try bundle.appendFont(allocator, id, font_data); + }, + .sound => |path| { + const sound_data = try readFileAll(allocator, asset_dir, path); + defer allocator.free(sound_data); + + try bundle.appendSound(allocator, id, sound_data); + }, + .texture => |opts| { + const texture_data = try readFileAll(allocator, asset_dir, opts.path); + defer allocator.free(texture_data); + + try bundle.appendTexture(allocator, id, texture_data, opts.info); + } + } + } + + const output_file = try std.fs.cwd().createFile(output_path, .{ }); + defer output_file.close(); + + var buffer: [4096 * 4]u8 = undefined; + var writer = output_file.writer(&buffer); + + try bundle.write(&writer.interface); + try writer.interface.flush(); +} diff --git a/src/game.zig b/src/game.zig index e63dc6d..c6ba37a 100644 --- a/src/game.zig +++ b/src/game.zig @@ -2,8 +2,8 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; -const Engine = @import("engine"); -const FontId = Engine.FontId; +const Engine = @import("engine_lib"); +const FontId = Engine.Font.Id; const Vec2 = Engine.Math.Vec2; const rgb = Engine.Math.rgb; @@ -20,6 +20,7 @@ const AssetId = enum { font_bold, font_italic, sound_wood, + texture_icon, }; const State = struct { @@ -54,6 +55,8 @@ pub fn loadAssets(assets: *Engine.Asset.Loader) callconv(.c) void { assets.addFont(@intFromEnum(AssetId.font_italic), "roboto-font/Roboto-Italic.ttf"); assets.addSound(@intFromEnum(AssetId.sound_wood), "wood01.ogg"); + + assets.addTexture(@intFromEnum(AssetId.texture_icon), "icon.png"); } pub fn deinit(state_ptr: *anyopaque) callconv(.c) void { @@ -98,13 +101,20 @@ pub fn tick(state_ptr: *anyopaque, frame: *Engine.Frame) callconv(.c) void { .rect = .{ .pos = .init(0, 0), .size = canvas_size }, .color = rgb(20, 20, 20) }); + + const icon = frame.assets.getTexture(@intFromEnum(AssetId.texture_icon)); + const size = Vec2.init(20, 20); frame.drawRectangle(.{ .rect = .{ .pos = state.player.sub(size.divideScalar(2)), .size = size }, - .color = rgb(200, 2, 200) + .color = rgb(200, 2, 200), + .sprite = .{ + .texture = icon, + .uv = .unit + } }); if (dir.x != 0 or dir.y != 0) { frame.drawRectanglOutline(state.player.sub(size.divideScalar(2)), size, rgb(20, 20, 20), 3);