From 6b48af7ea95c824913cedbc9b466cb4a7d27368f Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Sun, 15 Feb 2026 23:03:08 +0200 Subject: [PATCH] basic asset loading --- build.zig | 1 + engine/build.zig | 17 +- engine/src/lib/generational_array_list.zig | 11 +- engine/src/lib/root.zig | 153 ++++++++++- engine/src/runtime/audio.zig | 302 ++++++++++++++------- engine/src/runtime/engine.zig | 211 +++++++++----- engine/src/runtime/graphics.zig | 204 +++++++++----- src/game.zig | 70 +++-- 8 files changed, 693 insertions(+), 276 deletions(-) diff --git a/build.zig b/build.zig index 644d8f5..a1b1854 100644 --- a/build.zig +++ b/build.zig @@ -22,6 +22,7 @@ pub fn build(b: *std.Build) !void { .win32_has_console = b.option(bool, "console", "Show console (Window only)"), .win32_png_icon = b.path("src/assets/icon.png"), .hot_reload = b.option(bool, "hot-reload", "Dynamically load game code at runtime"), + .asset_dir = b.path("src/assets/"), }); { diff --git a/engine/build.zig b/engine/build.zig index c523147..59557bd 100644 --- a/engine/build.zig +++ b/engine/build.zig @@ -19,22 +19,27 @@ const InitOptions = struct { has_imgui: ?bool = null, has_tracy: ?bool = null, win32_has_console: ?bool = null, - win32_png_icon: ?std.Build.LazyPath = null + win32_png_icon: ?std.Build.LazyPath = null, + load_assets_at_runtime: ?bool = null, + asset_dir: std.Build.LazyPath, }; pub fn init(outer_builder: *std.Build, opts: InitOptions) void { const b = opts.dep_engine.builder; const isWasm = opts.target.result.cpu.arch.isWasm(); + const isDebug = (opts.optimize == .Debug); var has_tracy = false; var hot_reload = false; + var load_assets_at_runtime = false; if (!isWasm) { - hot_reload = opts.hot_reload orelse (opts.optimize == .Debug); - has_tracy = opts.has_tracy orelse (opts.optimize == .Debug); + hot_reload = opts.hot_reload orelse isDebug; + has_tracy = opts.has_tracy orelse isDebug; + load_assets_at_runtime = opts.load_assets_at_runtime orelse isDebug; } - const has_imgui = opts.has_imgui orelse (opts.optimize == .Debug); + const has_imgui = opts.has_imgui orelse isDebug; const statically_linked = opts.statically_linked orelse true; var build_options = b.addOptions(); @@ -42,6 +47,10 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void { build_options.addOption(bool, "has_tracy", has_tracy); build_options.addOption(bool, "hot_reload", hot_reload); build_options.addOption(bool, "statically_linked", statically_linked); + build_options.addOption(bool, "load_assets_at_runtime", load_assets_at_runtime); + if (load_assets_at_runtime) { + build_options.addOptionPath("asset_dir", opts.asset_dir); + } const engine_lib = b.createModule(.{ .root_source_file = b.path("src/lib/root.zig") diff --git a/engine/src/lib/generational_array_list.zig b/engine/src/lib/generational_array_list.zig index 41e14f4..c37db12 100644 --- a/engine/src/lib/generational_array_list.zig +++ b/engine/src/lib/generational_array_list.zig @@ -37,7 +37,7 @@ pub fn GenerationalArrayList( pub const Id = packed struct { pub const Int = @Type(.{ .int = .{ - .bits = @bitSizeOf(Index) + @bitSizeOf(Generation), + .bits = @bitSizeOf(Id), .signedness = .unsigned } }); @@ -62,6 +62,10 @@ pub fn GenerationalArrayList( pub fn asInt(self: Id) Int { return @bitCast(self); } + + pub fn fromInt(self: Int) Id { + return @bitCast(self); + } }; pub const ItemWithId = struct { @@ -227,7 +231,6 @@ pub fn GenerationalArrayList( } self.markUnused(unused_index, false); - self.count += 1; const id = Id{ .index = @intCast(unused_index), @@ -324,7 +327,6 @@ pub fn GenerationalArrayList( .generations = generations.ptr, .unused = unused.ptr, .len = self.len, - .count = self.count, .capacity = self.capacity }; } @@ -332,7 +334,6 @@ pub fn GenerationalArrayList( pub fn getMetadata(self: *Self) Metadata { return Metadata{ .len = self.len, - .count = self.count }; } @@ -362,7 +363,6 @@ pub fn GenerationalArrayList( try reader.readSliceAll(self.unused[0..divCeilGeneration(metadata.len)]); self.len = metadata.len; - self.count = metadata.count; } }; } @@ -467,7 +467,6 @@ test "read & write" { try expectEqual(array_list1.getAssumeExists(id1).*, array_list2.getAssumeExists(id1).*); try expectEqual(array_list1.getAssumeExists(id2).*, array_list2.getAssumeExists(id2).*); try expectEqual(array_list1.getAssumeExists(id3).*, array_list2.getAssumeExists(id3).*); - try expectEqual(array_list1.count, array_list2.count); } test "clear retaining capacity" { diff --git a/engine/src/lib/root.zig b/engine/src/lib/root.zig index 0fca00c..eff0d3a 100644 --- a/engine/src/lib/root.zig +++ b/engine/src/lib/root.zig @@ -142,9 +142,21 @@ pub const MouseButton = enum { middle, }; -pub const TextureId = enum(u16) { _ }; -pub const FontId = enum(u16) { _ }; -pub const SoundId = enum(u16) { _ }; +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 Sprite = struct { texture: TextureId, @@ -287,7 +299,12 @@ pub const Frame = struct { screen_size: Vec2, clear_color: Vec4, - pub fn init(gpa: std.mem.Allocator, screen_size: Vec2) Frame { + assets: Asset.Registry, + + pub fn init( + gpa: std.mem.Allocator, + screen_size: Vec2 + ) Frame { return Frame{ .arena = std.heap.ArenaAllocator.init(gpa), .time_ns = 0, @@ -302,6 +319,7 @@ pub const Frame = struct { .audio_commands = .empty, .hide_cursor = false, .screen_size = screen_size, + .assets = .empty }; } @@ -468,7 +486,124 @@ pub const Frame = struct { pub const Init = struct { gpa: std.mem.Allocator, - seed: u64 + seed: u64, + + 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 { @@ -478,10 +613,12 @@ pub const Callbacks = struct { pub const TickFn = *const fn(state: State, frame: *Frame) callconv(.c) void; pub const DeinitFn = *const fn(state: State) callconv(.c) void; pub const DebugFn = *const fn(state: State, imgui: *ImGui) callconv(.c) void; + pub const LoadAssetsFn = *const fn(assets: *Asset.Loader) callconv(.c) void; init: InitFn, tick: TickFn, deinit: DeinitFn, + loadAssets: if (build_options.load_assets_at_runtime) LoadAssetsFn else void, debug: if (build_options.has_imgui) DebugFn else void }; @@ -489,7 +626,8 @@ pub fn exportCallbacksIfNeeded( comptime init: Callbacks.InitFn, comptime tick: Callbacks.TickFn, comptime deinit: Callbacks.DeinitFn, - comptime debug: Callbacks.DebugFn + comptime debug: Callbacks.DebugFn, + comptime assets: Callbacks.LoadAssetsFn, ) void { const builtin = @import("builtin"); if (builtin.output_mode != .Lib) { @@ -499,6 +637,9 @@ pub fn exportCallbacksIfNeeded( @export(init, .{ .name = "init", .linkage = .strong }); @export(tick, .{ .name = "tick", .linkage = .strong }); @export(deinit, .{ .name = "deinit", .linkage = .strong }); + if (build_options.load_assets_at_runtime) { + @export(assets, .{ .name = "loadAssets", .linkage = .strong }); + } if (build_options.has_imgui) { @export(debug, .{ .name = "debug", .linkage = .strong }); } diff --git a/engine/src/runtime/audio.zig b/engine/src/runtime/audio.zig index 13f64ac..237c9c2 100644 --- a/engine/src/runtime/audio.zig +++ b/engine/src/runtime/audio.zig @@ -31,21 +31,37 @@ const Sound = union(enum) { return self.sample_rate; } - pub fn streamChannel(self: Raw, opts: StreamOptions) []f32 { - assert(opts.sample_rate == self.sample_rate); // TODO: - assert(opts.channel_index < self.channels.len); // TODO: + fn streamChannel(self: Raw, buffer: []f32, cursor: u32, channel_index: usize) usize { + assert(channel_index < self.channels.len); // TODO: - const channel = self.channels[opts.channel_index]; + const channel = self.channels[channel_index]; var memcpy_len: usize = 0; - if (opts.cursor + opts.buffer.len <= self.sample_count) { - memcpy_len = opts.buffer.len; - } else if (opts.cursor < opts.sample_count) { - memcpy_len = opts.sample_count - opts.cursor; + if (cursor + buffer.len <= self.sample_count) { + memcpy_len = buffer.len; + } else if (cursor < self.sample_count) { + memcpy_len = self.sample_count - cursor; } - @memcpy(opts.buffer[0..memcpy_len], channel[opts.cursor..][0..memcpy_len]); - return opts.buffer[0..memcpy_len]; + @memcpy(buffer[0..memcpy_len], channel[cursor..][0..memcpy_len]); + return memcpy_len; + } + + fn streamChannels(self: Raw, opts: StreamOptions) usize { + var sample_count: ?usize = null; + for (0.., opts.channels) |i, channel| { + const channel_sample_count = self.streamChannel(channel[0..opts.channel_size], opts.cursor, i); + if (sample_count == null) { + sample_count = channel_sample_count; + } else { + assert(sample_count.? == channel_sample_count); + } + } + return sample_count orelse 0; + } + + fn deinit(self: Raw) void { + _ = self; // autofix } }; @@ -53,9 +69,10 @@ const Sound = union(enum) { alloc_buffer: []u8, stb_vorbis: STBVorbis, - fn init(arena: std.mem.Allocator, data: []const u8, temp_vorbis_alloc_buffer: []u8) Vorbis { + fn init(gpa: std.mem.Allocator, data: []const u8, temp_vorbis_alloc_buffer: []u8) !Vorbis { const temp_stb_vorbis = try STBVorbis.init(data, temp_vorbis_alloc_buffer); - const alloc_buffer = try arena.alloc(u8, temp_stb_vorbis.getMinimumAllocBufferSize()); + const alloc_buffer = try gpa.alloc(u8, temp_stb_vorbis.getMinimumAllocBufferSize()); + errdefer gpa.free(alloc_buffer); // This can't fail because `alloc_buffer` is guarenteed to be big enough // And there can't be a decode error because `temp_stb_vorbis` was successfully initialized @@ -76,25 +93,45 @@ const Sound = union(enum) { return info.sample_rate; } - pub fn streamChannel(self: Vorbis, opts: StreamOptions) []f32 { - _ = self; // autofix - _ = opts; // autofix + fn getChannels(self: Vorbis) u32 { + const info = self.stb_vorbis.getInfo(); + return info.channels; + } + + fn streamChannels(self: Vorbis, opts: StreamOptions) usize { + self.stb_vorbis.seek(opts.cursor) catch |e| { + log.warn("Failed to seek vorbis: {}", .{e}); + return 0; + }; + // self.stb_vorbis.getSamples(); + @panic("TODO"); } + + fn deinit(self: Vorbis, gpa: std.mem.Allocator) void { + gpa.free(self.alloc_buffer); + } }; const StreamOptions = struct { - buffer: []f32, - cursor: u32, - channel_index: u32, - sample_rate: u32 + channels: []const [*]f32, + channel_size: usize, + cursor: u32 }; - pub fn streamChannel(self: Sound, opts: StreamOptions) []f32 { + pub fn deinit(self: Sound, gpa: std.mem.Allocator) void { + switch (self) { + .nil => {}, + .raw => |raw| raw.deinit(), + .vorbis => |vorbis| vorbis.deinit(gpa) + } + } + + pub fn streamChannels(self: Sound, opts: StreamOptions) usize { return switch (self) { - .nil => opts.buffer[0..0], - .raw => |raw| raw.streamChannel(opts), - .vorbis => |vorbis| vorbis.streamChannel(opts), + .nil => 0, + .raw => |raw| raw.streamChannels(opts), + .vorbis => |vorbis| vorbis.streamChannels(opts), }; } @@ -212,14 +249,28 @@ const ThreadState = struct { } }; -const SoundsList = Lib.GenerationalArrayList(u8, u8, Sound); +const SokolSetupOptions = struct { + logger: saudio.Logger, + channels: u32, + buffer_frames: u32 +}; + +const SoundList = Lib.GenerationalArrayList(u8, u8, Sound); +comptime { + assert(@bitSizeOf(SoundList.Id) == @bitSizeOf(Lib.SoundId)); +} gpa: std.mem.Allocator, running: std.atomic.Value(bool), temp_vorbis_alloc_buffer: []u8, +sokol_setup: bool, +sokol_options: SokolSetupOptions, + +// When possible hold `sounds_mutex` for as short as possbile, otherwise the audio thread will be affected. +sounds_mutex: std.Thread.Mutex, sounds_arena: std.heap.ArenaAllocator, -sounds: SoundsList, +sounds: SoundList, thread_state: ThreadState, @@ -234,8 +285,9 @@ const Options = struct { max_commands: u32 = 64, }; -pub fn init(self: *AudioSystem, opts: Options) !void { +pub fn init(opts: Options) !AudioSystem { const gpa = opts.allocator; + const temp_vorbis_alloc_buffer = try gpa.alloc(u8, opts.max_vorbis_alloc_buffer_size); errdefer gpa.free(temp_vorbis_alloc_buffer); @@ -246,15 +298,28 @@ pub fn init(self: *AudioSystem, opts: Options) !void { ); errdefer thread_state.deinit(gpa); - self.* = AudioSystem{ + return AudioSystem{ .gpa = gpa, .running = .init(false), .temp_vorbis_alloc_buffer = temp_vorbis_alloc_buffer, .sounds = .empty, .sounds_arena = std.heap.ArenaAllocator.init(gpa), .thread_state = thread_state, + .sounds_mutex = .{}, + .sokol_setup = false, + .sokol_options = .{ + .logger = opts.logger, + .channels = opts.channels, + .buffer_frames = opts.buffer_frames + } }; +} +/// IMPORTANT! After this function is called, the `self` pointer can't be moved +pub fn setupSokol(self: *AudioSystem) void { + assert(!self.sokol_setup); + + const opts = self.sokol_options; saudio.setup(.{ .stream_userdata_cb = sokolStreamCallback, .user_data = self, @@ -262,6 +327,7 @@ pub fn init(self: *AudioSystem, opts: Options) !void { .num_channels = @intCast(opts.channels), .buffer_frames = @intCast(opts.buffer_frames), }); + self.sokol_setup = true; self.running.store(true, .seq_cst); const sample_rate: f32 = @floatFromInt(saudio.sampleRate()); @@ -277,7 +343,9 @@ pub fn init(self: *AudioSystem, opts: Options) !void { pub fn deinit(self: *AudioSystem) void { self.running.store(false, .seq_cst); - saudio.shutdown(); + if (self.sokol_setup) { + saudio.shutdown(); + } self.thread_state.deinit(self.gpa); self.gpa.free(self.temp_vorbis_alloc_buffer); @@ -285,7 +353,24 @@ pub fn deinit(self: *AudioSystem) void { self.sounds.deinit(self.gpa); } -pub const LoadOptions = struct { +pub fn addSound(self: *AudioSystem) !Lib.SoundId { + self.sounds_mutex.lock(); + defer self.sounds_mutex.unlock(); + + const sound_id = try self.sounds.insert(self.gpa, .{ + .nil = {} + }); + return @enumFromInt(sound_id.asInt()); +} + +pub fn removeSound(self: *AudioSystem, id: Lib.SoundId) void { + self.sounds_mutex.lock(); + defer self.sounds_mutex.unlock(); + + _ = self.sounds.remove(.fromInt(@intFromEnum(id))); +} + +pub const DataOptions = struct { const PlaybackStyle = enum { stream, decode_once, @@ -303,66 +388,64 @@ pub const LoadOptions = struct { playback_style: ?PlaybackStyle = null, }; -pub fn load(self: *AudioSystem, opts: LoadOptions) !Lib.SoundId { - _ = self; // autofix - _ = opts; // autofix - @panic("TODO"); +pub fn setSoundData(self: *AudioSystem, id: Lib.SoundId, opts: DataOptions) !void { + const sound = self.sounds.get(.fromInt(@intFromEnum(id))) orelse return error.SoundNotFound; - // const id = self.sounds.items.len; - // try self.sounds.ensureUnusedCapacity(self.gpa, 1); - // - // // const list = std.ArrayList(u32).initBuffer(&.{}); - // // list.unused - // - // const arena_allocator = self.sounds_arena.allocator(); - // - // assert(opts.format == .vorbis); - // // const vorbis = try Sound.Vorbis.init(arena_allocator, opts.data, self.temp_vorbis_alloc_buffer); - // - // const PlaybackStyle = LoadOptions.PlaybackStyle; - // const temp_stb_vorbis = try STBVorbis.init(opts.data, self.temp_vorbis_alloc_buffer); - // const info = temp_stb_vorbis.getInfo(); - // const duration_in_samples = temp_stb_vorbis.getStreamLengthInSamples(); - // const decoded_size = info.channels * duration_in_samples * @sizeOf(f32); - // const stream_threshold = PlaybackStyle.stream_threshold; - // const default_playback_style: PlaybackStyle = if (decoded_size < stream_threshold) .decode_once else .stream; - // - // const playback_style = opts.playback_style orelse default_playback_style; - // if (playback_style == .decode_once) { - // const channels = try arena_allocator.alloc([*]f32, info.channels); - // for (channels) |*channel| { - // channel.* = (try arena_allocator.alloc(f32, duration_in_samples)).ptr; - // } - // const samples_decoded = temp_stb_vorbis.getSamples(channels, duration_in_samples); - // assert(samples_decoded == duration_in_samples); - // - // self.sounds.appendAssumeCapacity(Sound{ - // .raw = .{ - // .channels = channels, - // .sample_count = duration_in_samples, - // .sample_rate = info.sample_rate - // } - // }); - // } else { - // self.sounds.appendAssumeCapacity(Sound{ - // .vorbis = try Sound.Vorbis.init(arena_allocator, opts.data, self.temp_vorbis_alloc_buffer) - // }); - // } - // - // return @enumFromInt(id); -} + assert(opts.format == .vorbis); + const vorbis = try Sound.Vorbis.init(self.gpa, opts.data, self.temp_vorbis_alloc_buffer); -pub fn unload(self: *AudioSystem, id: Lib.SoundId) void { - _ = self; // autofix - _ = id; // autofix - @panic("TODO"); + const PlaybackStyle = DataOptions.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; + + playback_style = default_syle; + } else { + playback_style = opts.playback_style.?; + } + + var new_sound: Sound = .nil; + errdefer new_sound.deinit(self.gpa); + + if (playback_style == .decode_once) { + const duration_in_samples = vorbis.getSampleCount(); + + const arena_allocator = self.sounds_arena.allocator(); + const channels = try arena_allocator.alloc([*]f32, vorbis.getChannels()); + for (channels) |*channel| { + channel.* = (try arena_allocator.alloc(f32, duration_in_samples)).ptr; + } + const samples_decoded = vorbis.stb_vorbis.getSamples(channels, duration_in_samples); + assert(samples_decoded == duration_in_samples); + + new_sound = Sound{ + .raw = .{ + .channels = channels, + .sample_count = duration_in_samples, + .sample_rate = vorbis.getSampleRate() + } + }; + vorbis.deinit(self.gpa); + } else { + new_sound = Sound{ .vorbis = vorbis }; + } + + { + self.sounds_mutex.lock(); + defer self.sounds_mutex.unlock(); + + sound.deinit(self.gpa); + sound.* = new_sound; + } } pub fn get(self: *AudioSystem, id: Lib.SoundId) Sound { return self.sounds.items[@intFromEnum(id)]; } -fn sokolStream(self: *AudioSystem, buffer: [*c]f32, num_frames: i32, num_channels: i32) void { +fn sokolStream(self: *AudioSystem, buffer: [*c]f32, num_frames: u32, num_channels: u32) void { if (!self.running.load(.seq_cst)) { return; } @@ -370,6 +453,9 @@ fn sokolStream(self: *AudioSystem, buffer: [*c]f32, num_frames: i32, num_channel const zone = tracy.initZone(@src(), .{ }); defer zone.deinit(); + self.sounds_mutex.lock(); + defer self.sounds_mutex.unlock(); + const thread_state = &self.thread_state; while (thread_state.commands.pop()) |command| { switch (command) { @@ -394,36 +480,46 @@ fn sokolStream(self: *AudioSystem, buffer: [*c]f32, num_frames: i32, num_channel assert(thread_state.temp_channel_buffer.len >= num_frames); @memset(buffer[0..@intCast(num_frames * num_channels)], 0); - _ = sample_rate; // autofix for (thread_state.instances.items) |*instance| { - _ = instance; // autofix - // const sound = self.sounds - // const audio_data = store.get(instance.data_id); - // const samples = audio_data.streamChannel(self.working_buffer[0..num_frames], instance.cursor, 0, sample_rate); - // for (0.., samples) |i, sample| { - // buffer[i] += sample * instance.volume; - // } - // instance.cursor += @intCast(samples.len); + const sound = self.sounds.get(.fromInt(@intFromEnum(instance.sound_id))) orelse continue; + assert(sound.getSampleRate() == sample_rate); // TODO: + + const sample_count = sound.streamChannels(.{ + .channels = &.{ + thread_state.temp_channel_buffer.ptr, + }, + .channel_size = num_frames, + .cursor = instance.cursor, + }); + + const samples = thread_state.temp_channel_buffer[0..sample_count]; + for (0.., samples) |i, sample| { + buffer[i] += sample * instance.volume; + } + instance.cursor += @intCast(samples.len); } { - // var i: usize = 0; - // while (i < thread_state.instances.items.len) { - // const instance = thread_state.instances.items[i]; - // const audio_data = store.get(instance.data_id); - // const is_complete = instance.cursor == audio_data.getSampleCount(); - // - // if (is_complete) { - // _ = self.instances.swapRemove(i); - // } else { - // i += 1; - // } - // } + var i: usize = 0; + while (i < thread_state.instances.items.len) { + const instance = thread_state.instances.items[i]; + + var is_complete = true; + if (self.sounds.get(.fromInt(@intFromEnum(instance.sound_id)))) |sound| { + is_complete = instance.cursor >= sound.getSampleCount(); + } + + if (is_complete) { + _ = thread_state.instances.swapRemove(i); + } else { + i += 1; + } + } } } fn sokolStreamCallback(buffer: [*c]f32, num_frames: i32, num_channels: i32, user_data: ?*anyopaque) callconv(.c) void { const audio: *AudioSystem = @alignCast(@ptrCast(user_data)); - audio.sokolStream(buffer, num_frames, num_channels); + audio.sokolStream(buffer, @intCast(num_frames), @intCast(num_channels)); } diff --git a/engine/src/runtime/engine.zig b/engine/src/runtime/engine.zig index fab3c2d..6b65e32 100644 --- a/engine/src/runtime/engine.zig +++ b/engine/src/runtime/engine.zig @@ -115,6 +115,7 @@ const GameLinking = struct { .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 {}, } }; @@ -129,12 +130,17 @@ const GameLinking = struct { } } + 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 = lib.lookup(GameCallbacks.InitFn, "init") orelse return error.MissingFunction, - .tick = lib.lookup(GameCallbacks.TickFn, "tick") orelse return error.MissingFunction, - .deinit = lib.lookup(GameCallbacks.DeinitFn, "deinit") orelse return error.MissingFunction, - .debug = if (build_options.has_imgui) lib.lookup(GameCallbacks.DebugFn, "debug") orelse return error.MissingFunction else {}, + .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 {}, }; } @@ -153,6 +159,8 @@ const GameLinking = struct { 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) { @@ -250,14 +258,13 @@ const GameState = struct { gpa: std.mem.Allocator, state: ?GameCallbacks.State, + frame: ?Lib.Frame, mouse_position: ?Vec2, - frame: Lib.Frame, started_at: std.time.Instant, last_frame_at: Lib.Nanoseconds, - seed: u64, - const Initial = struct { + const Init = struct { screen_size: Vec2, seed: u64 }; @@ -274,24 +281,31 @@ const GameState = struct { cursor_hidden: bool }; - pub fn init(gpa: std.mem.Allocator, initial: Initial) GameState { + pub fn init(gpa: std.mem.Allocator) GameState { return GameState{ .gpa = gpa, .mouse_position = null, - .frame = .init(gpa, initial.screen_size), .started_at = std.time.Instant.now() catch @panic("Instant.now() unsupported"), .last_frame_at = 0, - .seed = initial.seed, - .state = null + .state = null, + .frame = null, }; } + pub fn deinit(self: *GameState) void { + if (self.frame) |*frame| { + frame.deinit(); + } + } + fn processEvent(self: *GameState, event: Input.Event) void { - const frame = &self.frame; + assert(self.frame != null); + const frame = &self.frame.?; + switch (event) { .key_pressed => |opts| { if (!opts.repeat) { - frame.keyboard.press(opts.code, self.frame.time_ns); + frame.keyboard.press(opts.code, frame.time_ns); } }, .key_released => |key_code| { @@ -329,12 +343,21 @@ const GameState = struct { } } - fn runGameInit(self: *GameState, init_fn: GameCallbacks.InitFn) void { + fn runGameInit(self: *GameState, init_fn: GameCallbacks.InitFn, initial: Init, assets: []const Lib.Asset) void { + assert(self.frame == null); const opts = Lib.Init{ .gpa = self.gpa, - .seed = self.seed, + .seed = initial.seed, + .assets = .{ + .items = assets + } }; self.state = init_fn(&opts); + + self.frame = .init(self.gpa, initial.screen_size); + self.frame.?.assets = .{ + .items = assets + }; } fn runGameDeinit(self: *GameState, deinit_fn: GameCallbacks.DeinitFn) void { @@ -343,13 +366,9 @@ const GameState = struct { self.state = null; } - fn runGameTick(self: *GameState, tick_fn: GameCallbacks.TickFn) void { - assert(self.state != null); - tick_fn(self.state.?, &self.frame); - } - fn tick(self: *GameState, tick_fn: GameCallbacks.TickFn, opts: GameState.Tick) !TickResult { - const frame = &self.frame; + assert(self.frame != null); + const frame = &self.frame.?; _ = frame.arena.reset(.retain_capacity); const arena = frame.arena.allocator(); @@ -403,7 +422,8 @@ const GameState = struct { } } - self.runGameTick(tick_fn); + assert(self.state != null); + tick_fn(self.state.?, frame); if (maybe_screen_scaler) |screen_scaler| { screen_scaler.pop(frame, frame.clear_color); @@ -415,10 +435,10 @@ const GameState = struct { frame.mouse_button.released = .initEmpty(); return TickResult{ - .graphics = self.frame.graphics_commands.items, - .audio = self.frame.audio_commands.items, - .clear_color = self.frame.clear_color, - .cursor_hidden = self.frame.hide_cursor + .graphics = frame.graphics_commands.items, + .audio = frame.audio_commands.items, + .clear_color = frame.clear_color, + .cursor_hidden = frame.hide_cursor }; } @@ -426,19 +446,15 @@ const GameState = struct { assert(self.state != null); debug_fn(self.state.?, imgui_ctx); } - - pub fn deinit(self: *GameState) void { - self.frame.deinit(); - } }; const Recording = struct { arena: std.heap.ArenaAllocator, - initial: GameState.Initial, + initial: GameState.Init, ticks: std.ArrayList(GameState.Tick), - pub fn init(gpa: std.mem.Allocator, initial: GameState.Initial) Recording { + pub fn init(gpa: std.mem.Allocator, initial: GameState.Init) Recording { return Recording{ .arena = .init(gpa), .ticks = .empty, @@ -466,6 +482,7 @@ allocator: std.mem.Allocator, graphics: Graphics, audio: Audio, +assets: std.ArrayList(Lib.Asset), game_linking: GameLinking, game_state: GameState, @@ -497,18 +514,26 @@ pub fn run(self: *Engine, opts: RunOptions) !void { game_linking = try .initStatic(); } - const initial = GameState.Initial{ + const initial = GameState.Init{ .seed = @bitCast(std.time.milliTimestamp()), .screen_size = .initFromInt(u32, opts.screen_width, opts.screen_height) }; - var game_state: GameState = .init(opts.allocator, initial); - game_state.runGameInit(game_linking.callbacks.init); self.* = Engine{ .allocator = opts.allocator, - .graphics = undefined, - .audio = undefined, - .game_state = game_state, + .graphics = try .init(.{ + .allocator = opts.allocator, + .logger = .{ .func = sokolLogCallback }, + // TODO: + // .imgui_font = .{ + // .ttf_data = @embedFile("../assets/roboto-font/Roboto-Regular.ttf"), + // } + }), + .audio = try .init(.{ + .allocator = opts.allocator, + .logger = .{ .func = sokolLogCallback }, + }), + .game_state = .init(opts.allocator), .game_linking = game_linking, .show_debug = false, .restart_game = false, @@ -518,8 +543,65 @@ pub fn run(self: *Engine, opts: RunOptions) !void { .play_recording = false, .recording_tick = 0, .seed = initial.seed, + .assets = .empty }; + if (build_options.load_assets_at_runtime) { + var dir = try std.fs.cwd().openDir(build_options.asset_dir, .{ .iterate = true }); + defer dir.close(); + 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, .{ + .id = asset_desc.id, + .kind = .{ .font = font_id } + }); + }, + .texture => |path| { + _ = path; // autofix + }, + .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, .{ + .id = asset_desc.id, + .kind = .{ .sound = sound_id } + }); + }, + } + } + } + + self.game_state.runGameInit(game_linking.callbacks.init, initial, self.assets.items); + if (opts.do_recording) { self.recording = .init(opts.allocator, initial); } @@ -537,7 +619,12 @@ pub fn run(self: *Engine, opts: RunOptions) !void { log.debug("Build options:", .{}); inline for (@typeInfo(build_options).@"struct".decls) |decl| { - log.debug("- {s}: {}", .{decl.name, @field(build_options, decl.name)}); + const value = @field(build_options, decl.name); + if (@TypeOf(value) == []const u8) { + log.debug("- {s}: {s}", .{decl.name, value}); + } else { + log.debug("- {s}: {}", .{decl.name, value}); + } } // TODO: Don't hard code icon path, allow changing through options @@ -577,19 +664,10 @@ fn sokolInit(self: *Engine) !void { const zone = tracy.initZone(@src(), .{ }); defer zone.deinit(); - try self.graphics.init(.{ - .allocator = self.allocator, - .logger = .{ .func = sokolLogCallback }, - // TODO: - // .imgui_font = .{ - // .ttf_data = @embedFile("../assets/roboto-font/Roboto-Regular.ttf"), - // } - }); + try self.graphics.setupSokol(); + self.audio.setupSokol(); - try self.audio.init(.{ - .allocator = self.allocator, - .logger = .{ .func = sokolLogCallback }, - }); + try self.graphics.refreshFonts(); } fn sokolCleanup(self: *Engine) void { @@ -606,6 +684,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); } fn sokolFrame(self: *Engine) !void { @@ -653,27 +732,25 @@ fn sokolFrame(self: *Engine) !void { sapp.showMouse(!tick_result.cursor_hidden); { - self.graphics.beginFrame(); + try self.graphics.beginFrame(); defer self.graphics.endFrame(tick_result.clear_color); self.graphics.drawCommands(tick_result.graphics); if (self.show_debug and build_options.has_imgui) { - try self.showDebugWindow(&self.game_state.frame); + try self.showDebugWindow(); } } - // TODO: - // for (tick_result.audio) |command| { - // try self.audio.mixer.commands.push(command); - // } + for (tick_result.audio) |command| { + try self.audio.thread_state.commands.push(command); + } } if (self.restart_game) { self.restart_game = false; - - const initial = GameState.Initial{ + const initial = GameState.Init{ .seed = self.seed, .screen_size = .init(sapp.widthf(), sapp.heightf()) }; @@ -681,8 +758,8 @@ fn sokolFrame(self: *Engine) !void { self.game_state.runGameDeinit(self.game_linking.callbacks.deinit); self.game_state.deinit(); - self.game_state = .init(self.allocator, initial); - self.game_state.runGameInit(self.game_linking.callbacks.init); + self.game_state = .init(self.allocator); + self.game_state.runGameInit(self.game_linking.callbacks.init, initial, self.assets.items); if (self.recording) |recording| { recording.deinit(); @@ -691,7 +768,7 @@ fn sokolFrame(self: *Engine) !void { } } -fn showDebugWindow(self: *Engine, frame: *Lib.Frame) !void { +fn showDebugWindow(self: *Engine) !void { if (!imgui.beginWindow(.{ .name = "Debug", .pos = Vec2.init(20, 20), @@ -733,8 +810,8 @@ fn showDebugWindow(self: *Engine, frame: *Lib.Frame) !void { self.game_state.runGameDeinit(self.game_linking.callbacks.deinit); self.game_state.deinit(); - self.game_state = .init(self.allocator, recording.initial); - self.game_state.runGameInit(self.game_linking.callbacks.init); + self.game_state = .init(self.allocator); + self.game_state.runGameInit(self.game_linking.callbacks.init, recording.initial, self.assets.items); self.play_recording = true; self.recording_tick = 0; @@ -749,9 +826,9 @@ fn showDebugWindow(self: *Engine, frame: *Lib.Frame) !void { const time_ms: f64 = @floatFromInt(@divFloor(self.game_state.last_frame_at, std.time.ns_per_ms)); imgui.textFmt("Time: {:.2}", .{ time_ms / 1000 }); - imgui.textFmt("Draw commands: {}\n", .{ - frame.graphics_commands.items.len, - }); + // imgui.textFmt("Draw commands: {}\n", .{ + // frame.graphics_commands.items.len, + // }); // TODO: // imgui.textFmt("Audio instances: {}/{}\n", .{ // Audio.mixer.instances.items.len, diff --git a/engine/src/runtime/graphics.zig b/engine/src/runtime/graphics.zig index 5971028..6edec87 100644 --- a/engine/src/runtime/graphics.zig +++ b/engine/src/runtime/graphics.zig @@ -6,11 +6,13 @@ const sapp = sokol.app; const simgui = sokol.imgui; const sgl = sokol.gl; -const Math = @import("lib").Math; +const Lib = @import("lib"); +const Math = Lib.Math; const Vec2 = Math.Vec2; const Vec4 = Math.Vec4; const rgb = Math.rgb; const Rect = Math.Rect; +const Command = Lib.GraphicsCommand; const std = @import("std"); const log = std.log.scoped(.graphics); @@ -19,10 +21,6 @@ const assert = std.debug.assert; const imgui = @import("imgui.zig"); const tracy = @import("tracy"); const fontstash = @import("./fontstash/root.zig"); -pub const Font = fontstash.Font; - -const Lib = @import("lib"); -const Command = Lib.GraphicsCommand; const Graphics = @This(); @@ -33,26 +31,28 @@ const Graphics = @This(); // * https://github.com/libsdl-org/SDL/issues/11618 // * https://github.com/nimgl/nimgl/issues/59 -const Options = struct { - const ImguiFont = struct { - ttf_data: []const u8, - size: f32 = 16 - }; +pub const Sprite = Lib.Sprite; - allocator: std.mem.Allocator, - logger: sg.Logger = .{}, - imgui_font: ?ImguiFont = null +const SokolState = struct { + main_pipeline: sgl.Pipeline, + linear_sampler: sg.Sampler, + nearest_sampler: sg.Sampler, + font_context: fontstash.Context, +}; + +const ImguiFont = struct { + ttf_data: []const u8, + size: f32 = 16 +}; + +const SokolOptions = struct { + logger: sg.Logger, + imgui_font: ?ImguiFont, }; const Texture = struct { image: sg.Image, view: sg.View, - info: Info, - - const Info = struct { - width: u32, - height: u32, - }; const Data = struct { width: u32, @@ -60,16 +60,26 @@ const Texture = struct { rgba: [*]u8 }; }; -pub const TextureId = Lib.TextureId; -pub const TextureInfo = Texture.Info; -pub const Sprite = Lib.Sprite; +const Font = struct { + fontstash_id: ?fontstash.Font.Id, + data: ?[]const u8, -main_pipeline: sgl.Pipeline, -linear_sampler: sg.Sampler, -nearest_sampler: sg.Sampler, -font_context: fontstash.Context, + pub fn deinit(self: Font, gpa: std.mem.Allocator) void { + if (self.data) |data| { + gpa.free(data); + } + } +}; + +const FontArray = Lib.GenerationalArrayList(u8, u8, Font); +comptime { + assert(@bitSizeOf(FontArray.Id) == @bitSizeOf(Lib.FontId)); +} + +gpa: std.mem.Allocator, textures: std.ArrayList(Texture) = .empty, +fonts: FontArray = .empty, scale_stack_buffer: [32]Vec2 = undefined, scale_stack: std.ArrayList(Vec2) = .empty, @@ -77,18 +87,38 @@ scale_stack: std.ArrayList(Vec2) = .empty, scissor_stack_buffer: [32]Rect = undefined, scissor_stack: std.ArrayList(Rect) = .empty, -font_id_lookup: std.AutoArrayHashMapUnmanaged(Lib.FontId, fontstash.Font.Id) = .empty, +sokol_state: ?SokolState, +sokol_options: SokolOptions, + +const Options = struct { + allocator: std.mem.Allocator, + logger: sg.Logger = .{}, + imgui_font: ?ImguiFont = null +}; + +pub fn init(options: Options) !Graphics { + return Graphics{ + .gpa = options.allocator, + .sokol_state = null, + .sokol_options = .{ + .logger = options.logger, + .imgui_font = options.imgui_font, + } + }; +} + +pub fn setupSokol(self: *Graphics) !void { + const opts = self.sokol_options; -pub fn init(self: *Graphics, options: Options) !void { sg.setup(.{ - .logger = options.logger, + .logger = opts.logger, .environment = sglue.environment(), }); sgl.setup(.{ .logger = .{ - .func = options.logger.func, - .user_data = options.logger.user_data + .func = opts.logger.func, + .user_data = opts.logger.user_data } }); @@ -110,12 +140,12 @@ pub fn init(self: *Graphics, options: Options) !void { }, }); - imgui.setup(options.allocator, .{ + imgui.setup(self.gpa, .{ .logger = .{ - .func = options.logger.func, - .user_data = options.logger.user_data + .func = opts.logger.func, + .user_data = opts.logger.user_data }, - .no_default_font = options.imgui_font != null, + .no_default_font = opts.imgui_font != null, // TODO: Figure out a way to make imgui play nicely with UI // Ideally when mouse is inside a Imgui window, then the imgui cursor should be used. @@ -123,7 +153,7 @@ pub fn init(self: *Graphics, options: Options) !void { .disable_set_mouse_cursor = true }); - if (options.imgui_font) |imgui_font| { + if (opts.imgui_font) |imgui_font| { imgui.addFont(imgui_font.ttf_data, imgui_font.size); } @@ -149,7 +179,7 @@ pub fn init(self: *Graphics, options: Options) !void { .height = @intCast(atlas_dim), }); - self.* = Graphics{ + self.sokol_state = SokolState{ .main_pipeline = main_pipeline, .linear_sampler = linear_sampler, .nearest_sampler = nearest_sampler, @@ -158,15 +188,26 @@ pub fn init(self: *Graphics, options: Options) !void { } pub fn deinit(self: *Graphics, gpa: std.mem.Allocator) void { - self.font_id_lookup.deinit(gpa); + 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); + self.textures.deinit(gpa); - imgui.shutdown(); - self.font_context.deinit(); - sgl.shutdown(); - sg.shutdown(); } pub fn drawCommand(self: *Graphics, command: Command) void { + assert(self.sokol_state != null); + const sokol_state = self.sokol_state.?; + switch(command) { .push_transformation => |opts| { self.pushTransform(opts.translation, opts.scale); @@ -220,22 +261,24 @@ pub fn drawCommand(self: *Graphics, command: Command) void { sgl.scale(1/font_resolution_scale.x, 1/font_resolution_scale.y, 1); - const font_id = self.font_id_lookup.get(opts.font) orelse { + const font = self.fonts.get(.fromInt(@intFromEnum(opts.font))) orelse { log.warn("Attempt to use font that doesn't exist", .{}); return; }; - self.font_context.setFont(font_id); - self.font_context.setSize(opts.size * font_resolution_scale.y); - self.font_context.setAlign(.{ .x = .left, .y = .top }); - self.font_context.setSpacing(0); + const fontstash_id = font.fontstash_id orelse return; + const font_context = sokol_state.font_context; + font_context.setFont(fontstash_id); + font_context.setSize(opts.size * font_resolution_scale.y); + font_context.setAlign(.{ .x = .left, .y = .top }); + font_context.setSpacing(0); const r: u8 = @intFromFloat(opts.color.x * 255); const g: u8 = @intFromFloat(opts.color.y * 255); const b: u8 = @intFromFloat(opts.color.z * 255); const a: u8 = @intFromFloat(opts.color.w * 255); const color: u32 = r | (@as(u32, g) << 8) | (@as(u32, b) << 16) | (@as(u32, a) << 24); - self.font_context.setColor(color); - self.font_context.drawText( + font_context.setColor(color); + font_context.drawText( opts.pos.x * font_resolution_scale.x, opts.pos.y * font_resolution_scale.y, opts.text @@ -250,7 +293,21 @@ pub fn drawCommands(self: *Graphics, commands: []const Command) void { } } -pub fn beginFrame(self: *Graphics) 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.?; + imgui.newFrame(.{ .width = sapp.width(), .height = sapp.height(), @@ -264,14 +321,17 @@ pub fn beginFrame(self: *Graphics) void { self.scissor_stack = .initBuffer(&self.scissor_stack_buffer); self.scissor_stack.appendAssumeCapacity(.init(0, 0, sapp.widthf(), sapp.heightf())); - self.font_context.clearState(); + sokol_state.font_context.clearState(); sgl.defaults(); sgl.matrixModeProjection(); sgl.ortho(0, sapp.widthf(), sapp.heightf(), 0, -1, 1); - sgl.loadPipeline(self.main_pipeline); + sgl.loadPipeline(sokol_state.main_pipeline); } pub fn endFrame(self: *Graphics, clear_color: Vec4) void { + assert(self.sokol_state != null); + const sokol_state = self.sokol_state.?; + var pass_action: sg.PassAction = .{}; pass_action.colors[0] = sg.ColorAttachmentAction{ @@ -284,7 +344,7 @@ pub fn endFrame(self: *Graphics, clear_color: Vec4) void { } }; - self.font_context.flush(); + sokol_state.font_context.flush(); { sg.beginPass(.{ @@ -318,13 +378,16 @@ const Vertex = struct { uv: Vec2 }; -fn drawQuad(self: *Graphics, quad: [4]Vertex, color: Vec4, texture_id: TextureId) void { +fn drawQuad(self: *Graphics, quad: [4]Vertex, color: Vec4, texture_id: Lib.TextureId) void { + assert(self.sokol_state != null); + const sokol_state = self.sokol_state.?; + sgl.enableTexture(); defer sgl.disableTexture(); const view = self.textures.items[@intFromEnum(texture_id)].view; // TODO: Make sampler configurable - sgl.texture(view, self.nearest_sampler); + sgl.texture(view, sokol_state.nearest_sampler); sgl.beginQuads(); defer sgl.end(); @@ -402,10 +465,6 @@ fn drawLine(from: Vec2, to: Vec2, color: Vec4, width: f32) void { ); } -fn addFont(self: *Graphics, name: [*c]const u8, data: []const u8) !Font.Id { - return try self.font_context.addFont(name, data); -} - fn makeView(image: sg.Image) !sg.View { const image_view = sg.makeView(.{ .texture = .{ .image = image } @@ -448,7 +507,7 @@ fn makeImageWithMipMaps(mipmaps: []const Texture.Data) !sg.Image { return image; } -pub fn addTexture(self: *Graphics, gpa: std.mem.Allocator, mipmaps: []const Texture.Data) !TextureId { +pub fn loadTexture(self: *Graphics, gpa: std.mem.Allocator, mipmaps: []const Texture.Data) !Lib.TextureId { const image = try makeImageWithMipMaps(mipmaps); errdefer sg.deallocImage(image); @@ -469,7 +528,26 @@ pub fn addTexture(self: *Graphics, gpa: std.mem.Allocator, mipmaps: []const Text return @enumFromInt(index); } -pub fn getTextureInfo(self: *Graphics, id: TextureId) TextureInfo { - const texture = self.textures.items[@intFromEnum(id)]; - return texture.info; +pub fn addFont(self: *Graphics) !Lib.FontId { + const font_id = try self.fonts.insert(self.gpa, .{ + .fontstash_id = null, + .data = null + }); + return @enumFromInt(font_id.asInt()); +} + +pub fn removeFont(self: *Graphics, id: Lib.FontId) 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); + + if (font.data) |old_data| { + self.gpa.free(old_data); + } + font.data = new_data; + font.fontstash_id = null; + } } diff --git a/src/game.zig b/src/game.zig index aba0765..e63dc6d 100644 --- a/src/game.zig +++ b/src/game.zig @@ -13,43 +13,49 @@ const FontName = enum { regular, bold, italic, +}; - const EnumArray = std.EnumArray(FontName, FontId); +const AssetId = enum { + font_regular, + font_bold, + font_italic, + sound_wood, }; const State = struct { gpa: Allocator, player: Vec2, - - // font_id: FontName.EnumArray, - // wood01: Audio.Data.Id, }; pub fn init(opts: *const Engine.Init) callconv(.c) *anyopaque { const gpa = opts.gpa; - // const font_id_array: FontName.EnumArray = .init(.{ - // .regular = try Gfx.addFont("regular", @embedFile("assets/roboto-font/Roboto-Regular.ttf")), - // .bold = try Gfx.addFont("bold", @embedFile("assets/roboto-font/Roboto-Bold.ttf")), - // .italic = try Gfx.addFont("italic", @embedFile("assets/roboto-font/Roboto-Italic.ttf")), - // }); - - // const wood01 = try Audio.load(.{ - // .format = .vorbis, - // .data = @embedFile("assets/wood01.ogg"), - // }); - const state = gpa.create(State) catch @panic("OOM"); state.* = State{ .gpa = gpa, .player = .init(50, 50), - // .font_id = font_id_array, - // .wood01 = wood01 }; return state; } +fn getFont(assets: Engine.Asset.Registry, font_name: FontName) FontId { + const asset_id = switch (font_name) { + .regular => AssetId.font_regular, + .bold => AssetId.font_bold, + .italic => AssetId.font_italic, + }; + return assets.getFont(@intFromEnum(asset_id)); +} + +pub fn loadAssets(assets: *Engine.Asset.Loader) callconv(.c) void { + assets.addFont(@intFromEnum(AssetId.font_regular), "roboto-font/Roboto-Regular.ttf"); + assets.addFont(@intFromEnum(AssetId.font_bold), "roboto-font/Roboto-Bold.ttf"); + assets.addFont(@intFromEnum(AssetId.font_italic), "roboto-font/Roboto-Italic.ttf"); + + assets.addSound(@intFromEnum(AssetId.sound_wood), "wood01.ogg"); +} + pub fn deinit(state_ptr: *anyopaque) callconv(.c) void { const state: *State = @alignCast(@ptrCast(state_ptr)); const gpa = state.gpa; @@ -80,10 +86,10 @@ pub fn tick(state_ptr: *anyopaque, frame: *Engine.Frame) callconv(.c) void { dir = dir.normalized(); if (dir.x != 0 or dir.y != 0) { - // frame.playAudio(.{ - // .id = g_state.wood01, - // .volume = 0.1 - // }); + frame.playAudio(.{ + .id = frame.assets.getSound(@intFromEnum(AssetId.sound_wood)), + .volume = 0.1 + }); } state.player = state.player.add(dir.multiplyScalar(50 * dt)); @@ -104,11 +110,21 @@ pub fn tick(state_ptr: *anyopaque, frame: *Engine.Frame) callconv(.c) void { frame.drawRectanglOutline(state.player.sub(size.divideScalar(2)), size, rgb(20, 20, 20), 3); } - // const regular_font = g_state.font_id.get(.regular); - // frame.drawText(g_state.player, "Player", .{ - // .font = regular_font, - // .size = 10 - // }); + if (frame.mouse_position) |mouse_position| { + const mouse_size = Vec2.init(10, 10); + frame.drawRectangle(.{ + .rect = .{ + .pos = mouse_position.sub(mouse_size.divideScalar(2)), + .size = mouse_size + }, + .color = rgb(20, 2, 200) + }); + } + + frame.drawText(state.player, "Player", .{ + .font = getFont(frame.assets, .regular), + .size = 10 + }); } pub fn debug(state_ptr: *anyopaque, imgui: *Engine.ImGui) callconv(.c) void { @@ -118,5 +134,5 @@ pub fn debug(state_ptr: *anyopaque, imgui: *Engine.ImGui) callconv(.c) void { } comptime { - Engine.exportCallbacksIfNeeded(init, tick, deinit, debug); + Engine.exportCallbacksIfNeeded(init, tick, deinit, debug, loadAssets); }