From 3cfd12d8ba3a9b4f67f2c092cc53720ab2b4fe92 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Mon, 10 Aug 2026 01:54:38 +0300 Subject: [PATCH] add nil ids to slotmap --- src/app.zig | 6 +- src/platform/audio.zig | 82 ++++++++++++++++++++ src/platform/graphics.zig | 159 ++++++-------------------------------- src/platform/root.zig | 5 ++ src/platform/slot_map.zig | 23 +++++- 5 files changed, 134 insertions(+), 141 deletions(-) create mode 100644 src/platform/audio.zig diff --git a/src/app.zig b/src/app.zig index 706a0b5..d7b46ff 100644 --- a/src/app.zig +++ b/src/app.zig @@ -62,7 +62,7 @@ const Tilesheet = struct { pub fn init(gpa: Allocator, image: Gfx.ImageData, width: u32, height: u32, tile_size: Vec2) !Tilesheet { const sprites = try gpa.alloc(Gfx.Sprite.Id, width * height); errdefer gpa.free(sprites); - @memset(sprites, Gfx.getNilSprite()); + @memset(sprites, .nil); return Tilesheet{ .sprites = sprites, @@ -82,11 +82,11 @@ const Tilesheet = struct { pub fn get(self: Tilesheet, x: u32, y: u32) Gfx.Sprite.Id { if (x >= self.width or y >= self.height) { log.warn("Attempt to get tile which is out of bounds", .{}); - return Gfx.getNilSprite(); + return .nil; } const sprite_index = y * self.width + x; - if (self.sprites[sprite_index] == Gfx.getNilSprite()) { + if (self.sprites[sprite_index] == Gfx.Sprite.Id.nil) { const sprite = Gfx.initSprite(.{ .padding = 1 }); Gfx.setSprite(sprite, .{ .image = self.image, diff --git a/src/platform/audio.zig b/src/platform/audio.zig new file mode 100644 index 0000000..5d9ffb6 --- /dev/null +++ b/src/platform/audio.zig @@ -0,0 +1,82 @@ +const std = @import("std"); +const log = std.log.scoped(.audio); + +const sokol = @import("sokol"); +const sapp = sokol.app; +const saudio = sokol.audio; + +const tracy = @import("tracy"); + +const SlotMapType = @import("./slot_map.zig").SlotMapType; + +const State = struct { + sounds: Sound.SlotMap +}; + +var g_state: State = undefined; + +pub const InitOptions = struct { + logger: saudio.Logger = .{}, + buffer_frames: u32 = 2048, + max_sounds: usize = 128, +}; + +pub const Sound = struct { + cb: Callback, + userdata: *anyopaque, + volume: f32 = 1, + + pub const Callback = *const fn(sound: *Sound, samples: *std.ArrayList(f32)) void; + + const SlotMap = SlotMapType(u8, u16, Sound); + pub const Id = SlotMap.Id; +}; + +pub fn init(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); + self.* = State{ + .sounds = .init(sounds_buffer) + }; + + saudio.setup(.{ + .stream_userdata_cb = sokolStreamCallback, + .logger = opts.logger, + .num_channels = channels, + .buffer_frames = @intCast(opts.buffer_frames), + }); + + const sample_rate: f32 = @floatFromInt(saudio.sampleRate()); + const max_latency_s: f32 = @as(f32, @floatFromInt(opts.buffer_frames)) / sample_rate; + const max_latency_ns: i64 = @intFromFloat(max_latency_s * std.time.ns_per_s); + + log.debug("Init:", .{}); + log.debug("- sample_rate: {}", .{saudio.sampleRate()}); + log.debug("- channels: {}", .{saudio.channels()}); + log.debug("- buffer_frames: {}", .{saudio.bufferFrames()}); + log.debug("- max_latency: {f}", .{std.Io.Duration.fromNanoseconds(max_latency_ns)}); +} + +pub fn deinit(gpa: std.mem.Allocator) void { + var self = &g_state; + + saudio.shutdown(); + + gpa.free(self.sounds.slots.allocatedSlice()); +} + +pub fn addSound() Sound.Id { +} + +pub fn removeSound() void { +} + +fn sokolStreamCallback(buffer: [*c]f32, num_frames: i32, num_channels: i32, user_data: ?*anyopaque) callconv(.c) void { + _ = buffer; // autofix + _ = num_frames; // autofix + _ = num_channels; // autofix + _ = user_data; // autofix +} diff --git a/src/platform/graphics.zig b/src/platform/graphics.zig index 487e3f6..1ffd88d 100644 --- a/src/platform/graphics.zig +++ b/src/platform/graphics.zig @@ -49,19 +49,15 @@ const State = struct { textures_buffer: [Texture.max_textures]Texture.SlotMap.Slot, textures: Texture.SlotMap, - nil_texture: Texture.Id, spritesheets_buffer: [Spritesheet.max_spritesheets]Spritesheet.SlotMap.Slot, spritesheets: Spritesheet.SlotMap, - nil_spritesheet: Spritesheet.Id, sprites_buffer: [Sprite.max_sprites]Sprite.SlotMap.Slot, sprites: Sprite.SlotMap, - nil_sprite: Sprite.Id, fonts_buffer: [Font.max_fonts]Font.SlotMap.Slot, fonts: Font.SlotMap, - nil_font: Font.Id, }; var g_state: State = undefined; @@ -173,67 +169,17 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void { .textures_buffer = undefined, .textures = .init(&self.textures_buffer), - .nil_texture = undefined, .sprites_buffer = undefined, .sprites = .init(&self.sprites_buffer), - .nil_sprite = undefined, .spritesheets_buffer = undefined, .spritesheets = .init(&self.spritesheets_buffer), - .nil_spritesheet = undefined, .fonts_buffer = undefined, .fonts = .init(&self.fonts_buffer), - .nil_font = undefined, }; - self.nil_texture = self.textures.insertAssumeCapacity(); - { - const nil_texture = self.textures.getAssumeExists(self.nil_texture); - nil_texture.* = Texture{ - .dynamic = false, - .updated_at = null, - .image = .{ .id = sg.invalid_id }, - .view = .{ .id = sg.invalid_id }, - }; - } - - self.nil_spritesheet = self.spritesheets.insertAssumeCapacity(); - { - const nil_spritesheet = self.spritesheets.getAssumeExists(self.nil_spritesheet); - nil_spritesheet.* = Spritesheet{ - .texture = self.nil_texture, - .format = .rgba8, - .size = .init(0, 0), - .needs_repack = false, - .needs_texture_rebuild = false, - .nodes = undefined, - .packer = null, - }; - } - - self.nil_sprite = self.sprites.insertAssumeCapacity(); - { - const nil_sprite = self.sprites.getAssumeExists(self.nil_sprite); - nil_sprite.* = Sprite{ - .spritesheet = self.nil_spritesheet, - .data = .none, - .position = null, - .padding = 0 - }; - } - - self.nil_font = self.fonts.insertAssumeCapacity(); - { - const nil_font = self.fonts.getAssumeExists(self.nil_font); - nil_font.* = Font{ - .spritesheet = self.nil_spritesheet, - .stb = null, - .cache = .init(), - }; - } - self.default_spritesheet = initSpritesheet(.{}); self.default_font_spritesheet = initSpritesheet(.{ .format = .r8 }); @@ -612,13 +558,13 @@ pub fn initTexture(opts: TextureOptions) Texture.Id { const self = &g_state; const id = self.textures.insertUndefined() catch { log.warn("Failed to create texture, limit reached! limit: {}", .{self.textures.slots.capacity}); - return self.nil_texture; + return .nil; }; const view = sg.allocView(); if (sg.queryViewState(view) != .ALLOC) { log.warn("Failed to alloc texture view", .{}); - return self.nil_texture; + return .nil; } const texture = self.textures.getAssumeExists(id); @@ -665,10 +611,6 @@ pub fn deinitTexture(id: Texture.Id) void { pub fn setTexture(id: Texture.Id, texture_data: ImageData) void { const self = &g_state; - if (id == self.nil_texture) { - return; - } - const texture = self.textures.get(id) orelse { log.warn("Attempt to set texture that doesn't exist: {}", .{id}); return; @@ -808,8 +750,7 @@ pub fn draw(opts: DrawOptions) void { var quad = opts.quad; var view: sg.View = .{ .id = sg.invalid_id }; - if (opts.texture != self.nil_texture and self.textures.exists(opts.texture)) { - const texture = self.textures.getAssumeExists(opts.texture); + if (self.textures.get(opts.texture)) |texture| { view = texture.view; } @@ -862,17 +803,11 @@ const SpriteOptions = struct { padding: u32 = 0, }; -// TODO: I don't like this getter, maybe nil_sprite should be a public global variable. -pub fn getNilSprite() Sprite.Id { - const self = &g_state; - return self.nil_sprite; -} - pub fn initSprite(opts: SpriteOptions) Sprite.Id { const self = &g_state; const id = self.sprites.insertUndefined() catch { log.warn("Failed to create sprite, limit reached! limit: {}", .{self.sprites.slots.capacity}); - return self.nil_sprite; + return .nil; }; const sprite = self.sprites.getAssumeExists(id); @@ -888,10 +823,6 @@ pub fn initSprite(opts: SpriteOptions) Sprite.Id { pub fn deinitSprite(id: Sprite.Id) void { const self = &g_state; - if (id == self.nil_sprite) { - return; - } - if (self.sprites.exists(id)) { const sprite = self.sprites.getAssumeExists(id); if (sprite.data) |data| { @@ -912,10 +843,6 @@ const SetSpriteOptions = struct { pub fn setSprite(id: Sprite.Id, opts: SetSpriteOptions) void { const self = &g_state; - if (id == self.nil_sprite) { - return; - } - const sprite = self.sprites.get(id) orelse { log.warn("Attempt to set sprite that doesn't exist: {}", .{id}); return; @@ -1048,10 +975,6 @@ fn repackSpritesheet(id: Spritesheet.Id) void { fn repackSpritesheetIfNeeded(id: Spritesheet.Id) void { const self = &g_state; - if (id == self.nil_spritesheet) { - return; - } - const spritesheet = self.spritesheets.get(id) orelse { log.warn("Attempt to pack spritesheet that doesn't exist: {f}", .{id}); return; @@ -1066,10 +989,6 @@ fn repackSpritesheetIfNeeded(id: Spritesheet.Id) void { fn rebuildSpritesheetTextureIfNeeded(id: Spritesheet.Id) void { const self = &g_state; - if (id == self.nil_spritesheet) { - return; - } - const spritesheet = self.spritesheets.get(id) orelse return; if (!spritesheet.needs_texture_rebuild) { return; @@ -1156,10 +1075,6 @@ fn ensureSpritePacked(id: Sprite.Id) bool { pub fn getSpriteUVRect(id: Sprite.Id) ?Rect { const self = &g_state; - if (id == self.nil_sprite) { - return null; - } - const sprite = self.sprites.get(id) orelse return null; const sprite_data = sprite.data orelse return null; @@ -1195,18 +1110,16 @@ pub fn drawSpriteQuad(id: Sprite.Id, points: Quad, color: Color) void { quad.vertices[3].position = points.positions[3]; var texture: ?Texture.Id = null; - if (id != self.nil_sprite) { - if (getSpriteUVRect(id)) |sprite_uv| { - quad.setUVRect(sprite_uv); + if (getSpriteUVRect(id)) |sprite_uv| { + quad.setUVRect(sprite_uv); - const sprite = self.sprites.getAssumeExists(id); - const spritesheet = self.spritesheets.getAssumeExists(sprite.spritesheet); - texture = spritesheet.texture; - } + const sprite = self.sprites.getAssumeExists(id); + const spritesheet = self.spritesheets.getAssumeExists(sprite.spritesheet); + texture = spritesheet.texture; } draw(.{ - .texture = texture orelse self.nil_texture, + .texture = texture orelse .nil, .quad = quad }); } @@ -1224,7 +1137,7 @@ pub fn initSpritesheet(opts: SpriteSheetOptions) Spritesheet.Id { const self = &g_state; const id = self.spritesheets.insertUndefined() catch { log.warn("Failed to create spritesheet, limit reached! limit: {}", .{self.spritesheets.slots.capacity}); - return self.nil_spritesheet; + return .nil; }; const spritesheet = self.spritesheets.getAssumeExists(id); @@ -1243,10 +1156,6 @@ pub fn initSpritesheet(opts: SpriteSheetOptions) Spritesheet.Id { pub fn deinitSpritesheet(id: Spritesheet.Id) void { const self = &g_state; - if (id == self.nil_spritesheet) { - return; - } - if (self.spritesheets.get(id)) { const spritesheet = self.spritesheets.getAssumeExists(id); deinitTexture(spritesheet.texture); @@ -1258,7 +1167,7 @@ pub fn initFont() Font.Id { const self = &g_state; const id = self.fonts.insertUndefined() catch { log.warn("Failed to create font, limit reached! limit: {}", .{self.fonts.slots.capacity}); - return self.nil_font; + return .nil; }; const font = self.fonts.getAssumeExists(id); @@ -1273,10 +1182,6 @@ pub fn initFont() Font.Id { pub fn deinitFont(id: Font.Id) void { const self = &g_state; - if (id == self.nil_font) { - return; - } - if (self.fonts.get(id)) |font| { font.cache.deinit(); self.fonts.removeAssumeExists(id); @@ -1285,10 +1190,6 @@ pub fn deinitFont(id: Font.Id) void { pub fn setFont(id: Font.Id, ttf_data: [:0]const u8) void { const self = &g_state; - if (id == self.nil_font) { - return; - } - const font = self.fonts.get(id) orelse { log.warn("Attempt to set font that doesn't exist: {}", .{id}); return; @@ -1312,17 +1213,9 @@ pub fn setFont(id: Font.Id, ttf_data: [:0]const u8) void { font.cache = .init(); } -fn getFont(id: Font.Id) ?*Font { - const self = &g_state; - if (id == self.nil_font) { - return null; - } - - return self.fonts.get(id); -} - pub fn getGlyphIndex(id: Font.Id, codepoint: u21) ?GlyphIndex { - const font = getFont(id) orelse return null; + const self = &g_state; + const font = self.fonts.get(id) orelse return null; const stb_font = font.stb orelse return null; if (stb_font.findGlyphIndex(codepoint)) |index| { @@ -1334,14 +1227,16 @@ pub fn getGlyphIndex(id: Font.Id, codepoint: u21) ?GlyphIndex { } pub fn getFontPixelScale(id: Font.Id, height: f32) f32 { - const font = getFont(id) orelse return 0; + const self = &g_state; + const font = self.fonts.get(id) orelse return 0; const stb_font = font.stb orelse return 0; return stb_font.scaleForPixelHeight(height); } pub fn getFontAscent(id: Font.Id, height: f32) f32 { - const font = getFont(id) orelse return 0; + const self = &g_state; + const font = self.fonts.get(id) orelse return 0; const stb_font = font.stb orelse return 0; const vmetrics = stb_font.getFontVMetrics(); @@ -1351,7 +1246,7 @@ pub fn getFontAscent(id: Font.Id, height: f32) f32 { pub fn getGlyph(id: Font.Id, index: GlyphIndex, scale_x: f32, scale_y: f32) ?*Font.Glyph { const self = &g_state; - const font = getFont(id) orelse return null; + const font = self.fonts.get(id) orelse return null; const stb_font = font.stb orelse return null; // TODO: Allow specifying a different scale for x and y @@ -1374,7 +1269,7 @@ pub fn getGlyph(id: Font.Id, index: GlyphIndex, scale_x: f32, scale_y: f32) ?*Fo const box_width: u32 = @intCast(glyph.box.x1 - glyph.box.x0); const box_height: u32 = @intCast(glyph.box.y1 - glyph.box.y0); if (box_width > 0 and box_height > 0) { - assert(glyph.sprite == self.nil_sprite); + assert(glyph.sprite == Sprite.Id.nil); glyph.sprite = initSprite(.{ .spritesheet = font.spritesheet, .padding = 1 }); assert(box_width < Font.Glyph.max_width); @@ -1395,7 +1290,7 @@ pub fn getGlyph(id: Font.Id, index: GlyphIndex, scale_x: f32, scale_y: f32) ?*Fo ); setSprite(glyph.sprite, .{ .image = bitmap }); } else { - glyph.sprite = self.nil_sprite; + glyph.sprite = .nil; } } glyph.used_this_frame = true; @@ -1431,7 +1326,7 @@ pub const TextRunLayout = struct { pub fn next(self: *TextRunLayout, glyph_index: GlyphIndex) ?GlyphRect { const glyph = getGlyph(self.font, glyph_index, self.scale_x, self.scale_y) orelse return null; - const font = getFont(self.font) orelse return null; + const font = g_state.fonts.get(self.font) orelse return null; const stb_font = font.stb orelse return null; if (self.prev_glyph) |prev_glyph| { @@ -1494,8 +1389,6 @@ pub const DrawTextOptions = struct { }; pub fn drawText(id: Font.Id, opts: DrawTextOptions) void { - const self = &g_state; - var layout = TextRunLayout.init(id, opts.height); var pos = opts.pos; @@ -1513,9 +1406,6 @@ pub fn drawText(id: Font.Id, opts: DrawTextOptions) void { while (iter.nextCodepoint()) |codepoint| { const glyph_index = getGlyphIndex(id, codepoint) orelse continue; const glyph_layout = layout.next(glyph_index) orelse continue; - if (glyph_layout.sprite == self.nil_sprite) { - continue; - } drawSprite( glyph_layout.sprite, @@ -1619,9 +1509,6 @@ pub const TextBuilder = struct { pub fn draw(self: *TextBuilder, pos: Vec2) void { for (self.glyphs.items) |glyph| { - if (glyph.sprite == g_state.nil_sprite) { - continue; - } drawSprite(glyph.sprite, glyph.rect.pos.add(pos), glyph.rect.size, glyph.color); } } @@ -2276,7 +2163,7 @@ pub const Font = struct { .x1 = 0, .y1 = 0, }, - .sprite = g_state.nil_sprite, + .sprite = .nil, .used_this_frame = false }; } diff --git a/src/platform/root.zig b/src/platform/root.zig index 85e309f..8f4bdee 100644 --- a/src/platform/root.zig +++ b/src/platform/root.zig @@ -26,6 +26,7 @@ const shd = @import("shader"); pub const ImGUI = @import("./imgui.zig"); pub const Gfx = @import("./graphics.zig"); pub const Input = @import("./input.zig"); +pub const Audio = @import("./audio.zig"); const EmbeddedAssets = @import("embedded_assets"); @@ -66,6 +67,9 @@ fn PlatformType(App: type) type { try Gfx.init(self.gpa, .{ .func = sokolLogCallback }); + try Audio.init(self.gpa, .{ + .logger = .{ .func = sokolLogCallback } + }); ImGUI.init(.{ .func = sokolLogCallback }); @@ -445,6 +449,7 @@ fn PlatformType(App: type) type { ImGUI.deinit(); Gfx.deinit(); + Audio.deinit(self.gpa); } }; } diff --git a/src/platform/slot_map.zig b/src/platform/slot_map.zig index 0ee00cb..3d4dea9 100644 --- a/src/platform/slot_map.zig +++ b/src/platform/slot_map.zig @@ -50,8 +50,17 @@ pub fn SlotMapType(Index: type, Generation: type, Value: type) type { generation: Generation, index: Index, + pub const nil = Id{ + .generation = std.math.maxInt(Generation), + .index = std.math.maxInt(Index), + }; + pub fn format(self: Id, writer: *std.Io.Writer) std.Io.Writer.Error!void { - try writer.print("Id({s}){{ {}, {} }}", .{ @typeName(Value), self.index, self.generation }); + if (self == Id.nil) { + try writer.print("Id({s}){{ nil }}", .{ @typeName(Value) }); + } else { + try writer.print("Id({s}){{ {}, {} }}", .{ @typeName(Value), self.index, self.generation }); + } } }; @@ -129,10 +138,12 @@ pub fn SlotMapType(Index: type, Generation: type, Value: type) type { assert(!slot.used); slot.used = true; - return Id{ + const result = Id{ .index = @intCast(index), .generation = slot.generation }; + assert(result != Id.nil); + return result; } pub fn insertUndefined(self: *Self) Allocator.Error!Id { @@ -178,6 +189,14 @@ pub fn SlotMapType(Index: type, Generation: type, Value: type) type { slot.used = false; slot.generation +%= 1; self.insertHole(id.index); + + const next_id = Id{ + .generation = id.generation, + .index = id.index, + }; + if (next_id == Id.nil) { + slot.generation +%= 1; + } } pub fn remove(self: *Self, id: Id) bool {