diff --git a/assets/impact-sounds/License.txt b/assets/impact-sounds/License.txt new file mode 100644 index 0000000..4890fe5 --- /dev/null +++ b/assets/impact-sounds/License.txt @@ -0,0 +1,23 @@ + + + Impact Sounds (1.0) + + Created/distributed by Kenney (www.kenney.nl) + Creation date: 19-12-2019 + + ------------------------------ + + License: (Creative Commons Zero, CC0) + http://creativecommons.org/publicdomain/zero/1.0/ + + This content is free to use in personal, educational and commercial projects. + Support us by crediting Kenney or www.kenney.nl (this is not mandatory) + + ------------------------------ + + Donate: http://support.kenney.nl + Request: http://request.kenney.nl + Patreon: http://patreon.com/kenney/ + + Follow on Twitter for updates: + http://twitter.com/KenneyNL \ No newline at end of file diff --git a/assets/impact-sounds/footstep_concrete_000.ogg b/assets/impact-sounds/footstep_concrete_000.ogg new file mode 100644 index 0000000..163cc56 Binary files /dev/null and b/assets/impact-sounds/footstep_concrete_000.ogg differ diff --git a/build.zig b/build.zig index 8e2dd17..8e2997b 100644 --- a/build.zig +++ b/build.zig @@ -10,7 +10,8 @@ pub fn build(b: *std.Build) !void { "sokoban/sokoban_tilesheet.png", "roboto-font/Roboto-Regular.ttf", "tiled/map.tmx", - "tiled/tileset.tsx" + "tiled/tileset.tsx", + "impact-sounds/footstep_concrete_000.ogg" }; const asset_dir_realpath = try b.build_root.join(b.graph.arena, &.{ assets_dir }); diff --git a/src/app.zig b/src/app.zig index 0f76344..8f8809d 100644 --- a/src/app.zig +++ b/src/app.zig @@ -53,7 +53,10 @@ player_direction: Direction, player_frame_index: u32, animation_timer: Platform.Nanoseconds, -sound: Audio.Sound.Id, +footstep_buffer: Audio.BufferId, +footstep_sound: Audio.SoundId = .nil, + +sfx_bus: Audio.BusId, const Tilesheet = struct { image: Gfx.ImageData, @@ -164,10 +167,8 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 { } } - const sound = Audio.initSound(.{ - .cb = .{ .sample = sinSampleCallback }, - .playing = false, - .frequency = 1 + const footstep_sound = Audio.addBuffer(.{ + .vorbis = plt.assets.readFile("impact-sounds/footstep_concrete_000.ogg") }); self.* = App{ @@ -181,16 +182,13 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 { .player_frame_index = 0, .animation_timer = 0, .tilesheet = tilesheet, - .sound = sound + .footstep_buffer = footstep_sound, + .sfx_bus = Audio.addBus(.{ .label = "sfx" }) }; return null; } -fn sinSampleCallback(sound: *Audio.Sound) f32 { - return @sin(440 * sound.phase); -} - pub fn frame(self: *App, plt: Platform.Frame) !void { const input = plt.input; @@ -212,8 +210,13 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { dir = dir.normalized(); self.player_pos = self.player_pos.add(dir.multiplyScalar(50 * dt)); + if (input.isKeyPressed(.F)) { + Audio.stop(self.footstep_sound); + } + if (input.isKeyPressed(.E)) { - Audio.setPlaying(self.sound, !Audio.getPlaying(self.sound)); + Audio.stop(self.footstep_sound); + self.footstep_sound = Audio.play(.{ .buffer = self.footstep_buffer, .loop = true, .bus = self.sfx_bus }); } var new_direction = self.player_direction; diff --git a/src/platform/audio.zig b/src/platform/audio.zig index 3cf3664..a7f78b9 100644 --- a/src/platform/audio.zig +++ b/src/platform/audio.zig @@ -1,47 +1,65 @@ const std = @import("std"); const log = std.log.scoped(.audio); const Io = std.Io; +const Allocator = std.mem.Allocator; const assert = std.debug.assert; const sokol = @import("sokol"); const sapp = sokol.app; const saudio = sokol.audio; +const ImGUI = @import("imgui.zig"); const tracy = @import("tracy"); +const STBVorbis = @import("stb_vorbis"); +const Math = @import("math"); const SlotMapType = @import("./slot_map.zig").SlotMapType; -const State = struct { - io: Io, - mutex: std.Io.Mutex, - sounds: Sound.SlotMap, - volume: f32 = 0.1, - running: std.atomic.Value(bool) = .init(false), +const Bus = struct { + label: ?[]const u8 = null, + volume: f32 = 1, + parent_id: ?Bus.Id = null, - temp_buffer: []f32 + pub const Id = packed struct { + index: u16, + + pub const nil = Id{ .index = std.math.maxInt(u16) }; + }; }; +pub const BusId = Bus.Id; -var g_state: State = undefined; +const Buffer = struct { + samples: Samples, -pub const InitOptions = struct { - logger: saudio.Logger = .{}, - buffer_frames: u32 = 512, - max_sounds: usize = 128, + const Samples = struct { + len: usize, + left: [*]const f32, + + // If `right` is is null, then this is mono + right: ?[*]const f32, + + pub fn deinit(self: Samples, gpa: Allocator) void { + gpa.free(self.left[0..self.len]); + if (self.right) |right| { + gpa.free(right[0..self.len]); + } + } + }; + + pub const Id = packed struct { + index: u16, + + pub const nil = Id{ .index = std.math.maxInt(u16) }; + }; }; +pub const BufferId = Buffer.Id; -pub const Sound = struct { - cb: Callback, - userdata: ?*anyopaque, - volume: f32, - looping: bool, - adsr: ADSR, - playing: bool, - - phase_increment: f32 = 0, - duration_frames: u32 = 0, - - frame_index: u32 = 0, - phase: f32 = 0, +const Sound = struct { + cursor: usize, + buffer: BufferId, + volume: f32 = 1, + loop: bool = false, + bus: BusId, pub const ADSR = struct { attack: f32, @@ -49,7 +67,7 @@ pub const Sound = struct { sustain: f32, release: f32, - pub const default = ADSR{ + pub const identity = ADSR{ .attack = 0, .decay = 0, .sustain = 1, @@ -57,38 +75,84 @@ pub const Sound = struct { }; }; - pub const Callback = union(enum) { - sample: *const fn(sound: *Sound) f32, - block: *const fn(sound: *Sound, samples: *std.ArrayList(f32)) void, - }; + const SlotMap = SlotMapType(u16, u8, Sound); + const Id = SlotMap.Id; +}; +pub const SoundId = Sound.Id; - const SlotMap = SlotMapType(u8, u16, Sound); - pub const Id = SlotMap.Id; +const ThreadState = struct { + // When modifying any fields in this struct, you must first acquire the mutex. + mutex: std.Io.Mutex, + + sounds: Sound.SlotMap, +}; + +const State = struct { + io: Io, + gpa: Allocator, + running: std.atomic.Value(bool) = .init(false), + + vorbis_alloc_buffer: []u8, + + buses: std.ArrayList(Bus), + buffers: std.ArrayList(Buffer), + + thread_state: ThreadState +}; + +var g_state: State = undefined; +pub var main_bus: BusId = .nil; +const g_sample_rate = 44100; + +pub const InitOptions = struct { + logger: saudio.Logger = .{}, + buffer_frames: u32 = 512, + + max_buffers: usize = 128, + max_sounds: usize = 256, + max_buses: usize = 16, + max_vorbis_alloc_buffer_size: u32 = 1 * Math.bytes_per_mib, }; pub fn init(io: Io, gpa: std.mem.Allocator, opts: InitOptions) !void { - const channels = 1; // TODO: Stereo audio const self = &g_state; - const sounds_buffer = try gpa.alloc(Sound.SlotMap.Slot, opts.max_sounds); - errdefer gpa.free(sounds_buffer); + var buses = try std.ArrayList(Bus).initCapacity(gpa, opts.max_buses); + errdefer buses.deinit(gpa); - const temp_buffer = try gpa.alloc(f32, opts.buffer_frames); - errdefer gpa.free(temp_buffer); + var buffers = try std.ArrayList(Buffer).initCapacity(gpa, opts.max_buffers); + errdefer buffers.deinit(gpa); + + const sounds = try gpa.alloc(Sound.SlotMap.Slot, opts.max_sounds); + errdefer gpa.free(sounds); + + const vorbis_alloc_buffer = try gpa.alloc(u8, opts.max_vorbis_alloc_buffer_size); + errdefer gpa.free(vorbis_alloc_buffer); self.* = State{ .io = io, - .mutex = .init, - .sounds = .init(sounds_buffer), - .temp_buffer = temp_buffer, + .gpa = gpa, + .buffers = buffers, + .buses = buses, + .vorbis_alloc_buffer = vorbis_alloc_buffer, + .thread_state = .{ + .mutex = .init, + .sounds = .init(sounds), + } }; self.running.store(true, .seq_cst); + main_bus = addBus(.{ + .label = "main" + }); + + const channels = 2; saudio.setup(.{ .stream_cb = sokolStreamCallback, .logger = opts.logger, .num_channels = channels, .buffer_frames = @intCast(opts.buffer_frames), + .sample_rate = g_sample_rate }); log.debug("Init:", .{}); @@ -104,146 +168,359 @@ pub fn deinit(gpa: std.mem.Allocator) void { saudio.shutdown(); - gpa.free(self.sounds.slots.allocatedSlice()); - gpa.free(self.temp_buffer); + for (self.buffers.items) |buffer| { + buffer.samples.deinit(gpa); + } + self.buffers.deinit(gpa); + + self.buses.deinit(gpa); + gpa.free(self.thread_state.sounds.slots.allocatedSlice()); + gpa.free(self.vorbis_alloc_buffer); } -pub fn getSampleRate() f32 { - return @floatFromInt(saudio.sampleRate()); -} - -pub const SoundOptions = struct { - cb: Sound.Callback, - userdata: ?*anyopaque = null, - adsr: Sound.ADSR = .default, - volume: f32 = 1, - playing: bool = true, - looping: bool = false, - - frequency: ?f32 = null, - duration_ns: ?u64 = null +const BufferData = union(enum) { + raw: struct { + samples_mono: []f32, + sample_rate: u32 + }, + vorbis: []const u8 }; -pub fn initSound(opts: SoundOptions) Sound.Id { - var self = &g_state; +// Source: https://amini-allight.org/post/game-audio-programming-tutorial-part-6 +fn hermiteResample(input: []f32, output: []f32) void { + for (0..output.len) |i| { + const elapsed = @as(f32, @floatFromInt(i)) / @as(f32, @floatFromInt(output.len)); - self.mutex.lock(self.io) catch return .nil; - defer self.mutex.unlock(self.io); + const b_index: isize = @intFromFloat(elapsed * @as(f32, @floatFromInt(input.len))); + const a_index = b_index - 1; + const c_index = b_index + 1; + const d_index = b_index + 2; - const id = self.sounds.insertUndefined() catch { - log.warn("Sound limit reached! limit: {}", .{self.sounds.slots.capacity}); + const a = if (a_index >= 0 and a_index < input.len) input[@intCast(a_index)] else input[@intCast(b_index)]; + const b = input[@intCast(b_index)]; + const c = if (c_index >= 0 and c_index < input.len) input[@intCast(c_index)] else input[@intCast(b_index)]; + const d = if (d_index >= 0 and d_index < input.len) input[@intCast(d_index)] else input[@intCast(b_index)]; + + const t = (elapsed * @as(f32, @floatFromInt(input.len))) - @as(f32, @floatFromInt(b_index)); + + const c0 = b; + const c1 = (c - a) * 0.5; + const c2 = a - (b * 2.5) + (c * 2.0) - (d * 0.5); + const c3 = ((b - c) * 1.5) + ((d - a) * 0.5); + output[i] = (((((c3 * t) + c2) * t) + c1) * t) + c0; + } +} + +fn resampleIfNeeded(gpa: Allocator, samples: *[]f32, sample_rate: u32) !void { + const new_sample_count = samples.len * g_sample_rate / sample_rate; + if (new_sample_count != samples.len) { + const new_samples = try gpa.alloc(f32, new_sample_count); + hermiteResample(samples.*, new_samples); + gpa.free(samples.*); + samples.* = new_samples; + } +} + +fn bufferDataToSamples(data: BufferData) !Buffer.Samples { + const self = &g_state; + return switch (data) { + .raw => |raw| { + var samples = try self.gpa.dupe(f32, raw.samples_mono); + errdefer self.gpa.free(samples); + + try resampleIfNeeded(self.gpa, &samples, raw.sample_rate); + + return Buffer.Samples{ + .len = samples.len, + .left = samples.ptr, + .right = null + }; + }, + .vorbis => |vorbis_data| { + const vorbis = try STBVorbis.init(vorbis_data, self.vorbis_alloc_buffer); + const sample_count = vorbis.getStreamLengthInSamples(); + + const info = vorbis.getInfo(); + if (info.channels == 1) { + var samples = try self.gpa.alloc(f32, sample_count); + errdefer self.gpa.free(samples); + + const result = vorbis.getSamples(&.{ samples.ptr }, sample_count); + assert(result == sample_count); + + try resampleIfNeeded(self.gpa, &samples, info.sample_rate); + + return .{ + .len = samples.len, + .left = samples.ptr, + .right = null + }; + } else if (info.channels >= 2) { + var left_samples = try self.gpa.alloc(f32, sample_count); + errdefer self.gpa.free(left_samples); + + var right_samples = try self.gpa.alloc(f32, sample_count); + errdefer self.gpa.free(right_samples); + + const result = vorbis.getSamples(&.{ left_samples.ptr, right_samples.ptr }, sample_count); + assert(result == sample_count); + + try resampleIfNeeded(self.gpa, &left_samples, info.sample_rate); + try resampleIfNeeded(self.gpa, &right_samples, info.sample_rate); + + return Buffer.Samples{ + .len = left_samples.len, + .left = left_samples.ptr, + .right = right_samples.ptr + }; + } else { + return Buffer.Samples{ + .len = 0, + .left = &.{}, + .right = null + }; + } + } + }; +} + +pub fn addBuffer(data: BufferData) BufferId { + const self = &g_state; + + if (self.buffers.items.len == self.buffers.capacity) { + log.warn("Max audio buffers reached, limit: {}", .{self.buffers.capacity}); + return .nil; + } + + const samples = bufferDataToSamples(data) catch |e| { + log.err("Failed to add buffer: {}", .{e}); return .nil; }; - var phase_increment: f32 = 0; - if (opts.frequency) |frequency| { - phase_increment = frequency * 2 * std.math.pi / getSampleRate(); - } - - var duration_frames: u32 = 0; - if (opts.duration_ns) |duration_ns| { - duration_frames = @intCast(duration_ns * @as(u64, @intCast(saudio.sampleRate())) / std.time.ns_per_s); - } - - const sound = self.sounds.getAssumeExists(id); - sound.* = Sound{ - .cb = opts.cb, - .userdata = opts.userdata, - .adsr = opts.adsr, - .looping = opts.looping, - .volume = opts.volume, - .playing = opts.playing, - .duration_frames = duration_frames, - .phase_increment = phase_increment + const buffer_id = BufferId{ .index = @intCast(self.buffers.items.len) }; + const buffer = self.buffers.addOneAssumeCapacity(); + buffer.* = Buffer{ + .samples = samples }; - return id; + assert(buffer_id != BufferId.nil); + + return buffer_id; } -pub fn deinitSound(id: Sound.Id) void { - var self = &g_state; +pub fn addBus(opts: Bus) BusId { + const self = &g_state; - if (self.sounds.get(id)) |sound| { - self.mutex.lock(self.io) catch return; - defer self.mutex.unlock(self.io); + if (self.buses.items.len == self.buses.capacity) { + log.warn("Max audio buses reached, limit: {}", .{self.buses.capacity}); + return .nil; + } - _ = sound; // autofix - self.sounds.removeAssumeExists(id); + const bus_id = BusId{ .index = @intCast(self.buses.items.len) }; + const bus = self.buses.addOneAssumeCapacity(); + bus.* = opts; + + if (bus.parent_id != null and bus.parent_id == BusId.nil) { + bus.parent_id = null; + } + + if (bus.parent_id == null and main_bus != BusId.nil) { + bus.parent_id = main_bus; + } + + assert(bus_id != BusId.nil); + + return bus_id; +} + +fn busMultipliedVolume(id: BusId) f32 { + const self = &g_state; + + var result: f32 = 1; + var iter: ?BusId = id; + while (iter) |current| { + const bus = &self.buses.items[current.index]; + result *= bus.volume; + iter = bus.parent_id; + } + return result; +} + +const PlayOptions = struct { + buffer: BufferId, + // TODO: adsr: ?ADSR = null, + volume: f32 = 1, + loop: bool = false, + bus: ?BusId = null +}; + +pub fn play(opts: PlayOptions) SoundId { + const self = &g_state; + + var mutex = &self.thread_state.mutex; + var sounds = &self.thread_state.sounds; + + mutex.lock(self.io) catch |e| { + log.warn("Failed to lock mutex: {}", .{e}); + return .nil; + }; + defer mutex.unlock(self.io); + + const bus = opts.bus orelse main_bus; + if (bus == BusId.nil) { + return .nil; + } + + if (opts.buffer == BufferId.nil) { + return .nil; + } + + if (sounds.unusedCapacity() == 0) { + log.warn("Max sounds reached, limit: {}", .{sounds.slots.capacity}); + return .nil; + } + + const sound_id = sounds.insertAssumeCapacity(); + const sound = sounds.getAssumeExists(sound_id); + sound.* = Sound{ + .cursor = 0, + .volume = opts.volume, + .loop = opts.loop, + .buffer = opts.buffer, + .bus = bus, + }; + + return sound_id; +} + +pub fn stop(id: SoundId) void { + const self = &g_state; + + var mutex = &self.thread_state.mutex; + var sounds = &self.thread_state.sounds; + + mutex.lock(self.io) catch |e| { + log.warn("Failed to lock mutex: {}", .{e}); + return; + }; + defer mutex.unlock(self.io); + + _ = sounds.remove(id); +} + +pub fn getBus(id: BusId) ?*Bus { + const self = &g_state; + + if (id == .nil) { + return null; + } + return &self.buses.items[id.index]; +} + +pub fn showDebug() void { + const self = &g_state; + + if (ImGUI.beginWindow(.{ + .name = "audio", + .size = .init(200, 200) + })) { + defer ImGUI.endWindow(); + + const sounds = &self.thread_state.sounds; + const buses = &self.buses; + + ImGUI.text("Sounds: {}/{}", .{sounds.count(), sounds.slots.capacity}); + + if(ImGUI.beginTable("buses", 3, 0)) { + defer ImGUI.endTable(); + + ImGUI.tableSetupColumn("ID (Label)", 0); + ImGUI.tableSetupColumn("Volume", 0); + ImGUI.tableSetupColumn("Multiplied volume", 0); + ImGUI.tableHeadersRow(); + + for (0.., buses.items) |i, *bus| { + ImGUI.pushID(.{ .int = @intCast(i) }); + defer ImGUI.popID(); + + ImGUI.tableNextRow(); + + ImGUI.tableNextColumn(); + ImGUI.text("{} ({s})", .{i, bus.label orelse "-"}); + + ImGUI.tableNextColumn(); + _ = ImGUI.inputF32Drag(.{ + .label = "", + .value = &bus.volume, + .ex = .{ + .min = 0, + .max = 1, + .speed = 0.01 + } + }); + + ImGUI.tableNextColumn(); + ImGUI.text("{}", .{busMultipliedVolume(.{ .index = @intCast(i) })}); + } + } } } -pub fn setPlaying(id: Sound.Id, playing: bool) void { - var self = &g_state; - - if (self.sounds.get(id)) |sound| { - self.mutex.lock(self.io) catch return; - defer self.mutex.unlock(self.io); - - sound.playing = playing; - } -} - -pub fn getPlaying(id: Sound.Id) bool { - var self = &g_state; - - if (self.sounds.get(id)) |sound| { - return sound.playing; - } else { - return false; - } -} - -fn bumpSoundCursor(sound: *Sound, count: u32) void { - if (sound.duration_frames > 0) { - sound.frame_index += count; - sound.frame_index = @mod(sound.frame_index, sound.duration_frames); - } - - if (sound.phase_increment > 0) { - sound.phase += sound.phase_increment * @as(f32, @floatFromInt(count)); - sound.phase = @mod(sound.phase, 2 * std.math.pi); - } -} - -fn sokolStream(buffer: [*c]f32, num_frames: u32, num_channels: u32) !void { +fn sokolStream(output_buffer: [*c]f32, num_frames: u32, num_channels: u32) !void { const self = &g_state; if (!self.running.load(.seq_cst)) { return; } - try self.mutex.lock(self.io); - defer self.mutex.unlock(self.io); + const mutex = &self.thread_state.mutex; + const sounds = &self.thread_state.sounds; + const buses = self.buses; - assert(num_channels == 1); - assert(self.temp_buffer.len >= num_frames); + try mutex.lock(self.io); + defer mutex.unlock(self.io); - @memset(buffer[0..num_frames], 0); + assert(num_channels == 2); + @memset(output_buffer[0..(num_frames*2)], 0); - var iter = self.sounds.iterator(); - while (iter.next()) |id| { - const sound = self.sounds.getAssumeExists(id); - if (!sound.playing) { - continue; - } + var sound_iter = sounds.iterator(); + while (sound_iter.next()) |sound_id| { + const sound = sounds.getAssumeExists(sound_id); + const buffer = &self.buffers.items[sound.buffer.index]; - var samples = std.ArrayList(f32).initBuffer(self.temp_buffer[0..num_frames]); - switch (sound.cb) { - .block => |cb| { - cb(sound, &samples); - bumpSoundCursor(sound, @intCast(samples.items.len)); - }, - .sample => |cb| { - samples.expandToCapacity(); - for (samples.items) |*sample| { - sample.* = cb(sound); - bumpSoundCursor(sound, 1); + const bus_id = sound.bus; + const bus = buses.items[bus_id.index]; + _ = bus; // autofix + const bus_volume = busMultipliedVolume(bus_id); + + const volume = sound.volume * bus_volume; + + const left_channel = buffer.samples.left; + const right_channel = buffer.samples.right orelse buffer.samples.left; + + var output_index: usize = 0; + while (output_index < num_frames) { + while (sound.cursor < buffer.samples.len and output_index < num_frames) { + const left = left_channel[sound.cursor]; + const right = right_channel[sound.cursor]; + sound.cursor += 1; + + output_buffer[2*output_index+0] += left * volume; + output_buffer[2*output_index+1] += right * volume; + output_index += 1; + } + + if (!sound.loop) { + break; + } else { + if (sound.cursor == buffer.samples.len) { + sound.cursor = 0; } } } - for (0..samples.items.len) |i| { - buffer[i] += samples.items[i] * sound.volume * self.volume; + if (!sound.loop and sound.cursor == buffer.samples.len) { + sounds.removeAssumeExists(sound_id); } } } diff --git a/src/platform/imgui.zig b/src/platform/imgui.zig index 27a4132..ec6b515 100644 --- a/src/platform/imgui.zig +++ b/src/platform/imgui.zig @@ -503,3 +503,59 @@ pub fn collapsingHeader(label: []const u8) bool { ig.ImGuiTreeNodeFlags_None ); } + +pub fn beginTable(id: [*c]const u8, columns: u32, flags: ig.ImGuiTableFlags) bool { + if (isDisabled()) { + return false; + } + + return ig.igBeginTable(id, @intCast(columns), flags); +} + +pub fn endTable() void { + if (isDisabled()) { + return; + } + + ig.igEndTable(); +} + +pub fn tableNextColumn() void { + if (isDisabled()) { + return; + } + + _ = ig.igTableNextColumn(); +} + +pub fn tableNextRow() void { + if (isDisabled()) { + return; + } + + _ = ig.igTableNextRow(); +} + +pub fn tableSetColumnIndex(index: usize) void { + if (isDisabled()) { + return; + } + + _ = ig.igTableSetColumnIndex(@intCast(index)); +} + +pub fn tableSetupColumn(label: [*c]const u8, flags: ig.ImGuiTableColumnFlags) void { + if (isDisabled()) { + return; + } + + ig.igTableSetupColumn(label, flags); +} + +pub fn tableHeadersRow() void { + if (isDisabled()) { + return; + } + + ig.igTableHeadersRow(); +} diff --git a/src/platform/root.zig b/src/platform/root.zig index 720bdf3..8780ff7 100644 --- a/src/platform/root.zig +++ b/src/platform/root.zig @@ -141,6 +141,7 @@ fn PlatformType(App: type) type { Gfx.flush(.{}); Gfx.showDebug(); + Audio.showDebug(); ImGUI.endFrame(); Gfx.endFrame(); diff --git a/src/platform/slot_map.zig b/src/platform/slot_map.zig index 3d4dea9..62003ad 100644 --- a/src/platform/slot_map.zig +++ b/src/platform/slot_map.zig @@ -129,7 +129,11 @@ pub fn SlotMapType(Index: type, Generation: type, Value: type) type { pub fn unusedCapacity(self: *Self) usize { const capacity = @min(self.slots.capacity, std.math.maxInt(Index)); - return capacity - self.slots.items.len + self.hole_count; + return capacity - self.count(); + } + + pub fn count(self: *Self) usize { + return self.slots.items.len - self.hole_count; } pub fn insertAssumeCapacity(self: *Self) Id {