From 40fcba932364fe22ac7f1bba4af0b20e14635545 Mon Sep 17 00:00:00 2001 From: Szymon Nowakowski Date: Sat, 23 May 2026 01:37:28 +0200 Subject: [PATCH] Globalize textures, materials and gui --- src/AppContext.zig | 14 ++++++ src/Game.zig | 83 +++++++++++++++------------------ src/Player.zig | 7 +-- src/assets/Blocks.zig | 41 ++++------------ src/engine/Atom.zig | 17 +++---- src/engine/Atoms.zig | 23 ++++----- src/engine/DeviceAllocation.zig | 9 ++++ src/engine/Engine.zig | 2 - src/{ => engine}/Gui.zig | 65 ++++++++------------------ src/engine/Materials.zig | 72 ++++++++++++---------------- src/engine/Skybox.zig | 2 +- src/engine/Textures.zig | 79 +++++++++++++------------------ src/main.zig | 23 +++++++++ 13 files changed, 195 insertions(+), 242 deletions(-) rename src/{ => engine}/Gui.zig (87%) diff --git a/src/AppContext.zig b/src/AppContext.zig index 007dc5c..0070177 100644 --- a/src/AppContext.zig +++ b/src/AppContext.zig @@ -1,11 +1,20 @@ +//! This module contains some common global variables, which mostly refer to +//! singleton modules that are active throughout entire program's runtime. +//! +//! These globals can be safely referenced during and after game initialization. + const AppContext = @This(); const std = @import("std"); +const media = @import("media"); const glfw = @import("zglfw"); const Atoms = @import("engine/Atoms.zig"); const Engine = @import("engine/Engine.zig"); +const Gui = @import("engine/Gui.zig"); +const Materials = @import("engine/Materials.zig"); const Swapchain = @import("engine/Swapchain.zig"); +const Textures = @import("engine/Textures.zig"); const VkAllocator = @import("engine/VkAllocator.zig"); pub var allocator_general: std.mem.Allocator = undefined; @@ -13,8 +22,13 @@ pub var allocator_frame: std.mem.Allocator = undefined; pub var allocator_persistent: std.mem.Allocator = undefined; pub var io: std.Io = undefined; pub var vk_allocator: *VkAllocator = undefined; +pub var stbi: *media.stbi = undefined; pub var window: *glfw.Window = undefined; pub var atoms: *Atoms = undefined; pub var engine: *Engine = undefined; pub var swapchain: *Swapchain = undefined; + +pub var textures: *Textures = undefined; +pub var materials: *Materials = undefined; +pub var gui: *Gui = undefined; diff --git a/src/Game.zig b/src/Game.zig index 06b8f53..396a10a 100644 --- a/src/Game.zig +++ b/src/Game.zig @@ -16,7 +16,6 @@ const Chunk = @import("Chunk.zig"); const Chunks = @import("Chunks.zig"); const CommandBuffer = @import("engine/CommandBuffer.zig"); const Engine = @import("engine/Engine.zig"); -const Gui = @import("Gui.zig"); const Iterator2 = math.Iterator2; const Materials = @import("engine/Materials.zig"); const Player = @import("Player.zig"); @@ -44,14 +43,9 @@ directional_lights: shaders.DirectionalLightBuffer, sampler: vk.Sampler, deferred_command_buffers: std.ArrayList(CommandBuffer), -stbi: media.stbi, blocks: Blocks, -materials: Materials, -textures: Textures, chunks: Chunks, skybox: Skybox, -gui: Gui, - player: Player, const max_textures = 1024; @@ -64,18 +58,11 @@ const camera_near_plane = 0.1; pub fn init() !Game { const allocator_general = ctx.allocator_general; const allocator_frame = ctx.allocator_frame; - const io = ctx.io; const engine = ctx.engine; const swapchain = ctx.swapchain; - - var stbi = media.stbi.init(allocator_general, io); - errdefer stbi.deinit(); - - var materials = try Materials.init(); - errdefer materials.deinit(); - - var textures = try Textures.init(); - errdefer textures.deinit(); + const textures = ctx.textures; + const materials = ctx.materials; + const gui = ctx.gui; var blocks = try Blocks.init(); errdefer blocks.deinit(); @@ -83,8 +70,7 @@ pub fn init() !Game { // JANK HACK When this line is removed, capturing a frame with RenderDoc // will crash the game with segfault reading address 0x140 (presumably // within librenderdoc.so). - - blocks.loadAll(&materials, &textures, &stbi); + blocks.loadAll(); const sampler = try engine.createSampler(.{ .mag_filter = .linear, @@ -447,10 +433,10 @@ pub fn init() !Game { }); engine.setObjectName(global_descriptor_set, "DS Global", .{}); - const block_grass = try blocks.getOrLoad(&materials, &textures, &stbi, "Grass.json"); - const block_dirt = try blocks.getOrLoad(&materials, &textures, &stbi, "Dirt.json"); - const block_stone = try blocks.getOrLoad(&materials, &textures, &stbi, "Stone.json"); - const block_bedrock = try blocks.getOrLoad(&materials, &textures, &stbi, "Bedrock.json"); + const block_grass = try blocks.getOrLoad("Grass.json"); + const block_dirt = try blocks.getOrLoad("Dirt.json"); + const block_stone = try blocks.getOrLoad("Stone.json"); + const block_bedrock = try blocks.getOrLoad("Bedrock.json"); // VOLATILE Load all assets before this point @@ -640,18 +626,33 @@ pub fn init() !Game { .elements = directional_lights_data, }); - var skybox = try Skybox.load( - "skybox.hdr", - &stbi, - 512, - global_uniforms.buffer, - ); + var skybox = try Skybox.load("skybox.hdr", 512, global_uniforms.buffer); errdefer skybox.deinit(); - var gui = try Gui.init( - global_uniforms.buffer, - ); - errdefer gui.deinit(); + // TODO This coupling is not elegant - the game must provide the GUI with + // uniform buffer for globals. Maybe GUI should just use their own global + // uniform buffer not for any other reason than decoupling. After all, GUI + // only needs a single 3×2 matrix and does not need to depend on game's + // rendering pipeline. + try engine.updateDescriptorSets(.{ + .writes = &.{ + .{ + .dst_set = gui.box_descriptor_set, + .dst_binding = 0, + .dst_array_element = 0, + .descriptor_type = .uniform_buffer, + .descriptor_infos = .{ + .buffer = &.{ + .{ + .buffer = global_uniforms.buffer, + .offset = 0, + .range = vk.WHOLE_SIZE, + }, + }, + }, + }, + }, + }); return .{ .global_descriptor_set_layout = global_descriptor_set_layout, @@ -672,14 +673,9 @@ pub fn init() !Game { .sampler = sampler, .deferred_command_buffers = try .initCapacity(allocator_general, 4), - .stbi = stbi, .blocks = blocks, - .materials = materials, - .textures = textures, .chunks = .{ .chunks = chunks }, .skybox = skybox, - .gui = gui, - .player = .init(player_position_sv, 0, 0), }; } @@ -700,7 +696,6 @@ pub fn deinit(self: *Game) void { self.chunks.deinit(self.descriptor_pool); self.skybox.deinit(); - self.gui.deinit(); self.global_uniforms.deinit(); self.global_uniforms_staging_buffer.deinit(); @@ -719,17 +714,14 @@ pub fn deinit(self: *Game) void { engine.destroyDescriptorSetLayout(self.per_batch_descriptor_set_layout); engine.destroyDescriptorSetLayout(self.global_descriptor_set_layout); - self.textures.deinit(); - self.materials.deinit(); self.blocks.deinit(); - self.stbi.deinit(); self.* = undefined; } pub fn update(self: *Game, dt: f32) void { const swapchain = ctx.swapchain; + const gui = ctx.gui; - self.gui.beginFrame(); self.player.update(dt, &self.chunks); const extent = swapchain.extent; @@ -741,7 +733,7 @@ pub fn update(self: *Game, dt: f32) void { const crosshair_half_extent_px = 8; const crosshair_half_width_px = 1; - self.gui.pushBox(.{ + gui.pushBox(.{ .background_color = .init(1, 1, 1, 0.5), .border_color = .init(0, 0, 0, 1), .position_sspx = framebuffer_size.mulScalar(0.5).add(.init(-crosshair_half_extent_px, -crosshair_half_width_px)), @@ -750,7 +742,7 @@ pub fn update(self: *Game, dt: f32) void { .border_radius_px = 0, }); - self.gui.pushBox(.{ + gui.pushBox(.{ .background_color = .init(1, 1, 1, 0.5), .border_color = .init(0, 0, 0, 1), .position_sspx = framebuffer_size.mulScalar(0.5).add(.init(-crosshair_half_width_px, -crosshair_half_extent_px)), @@ -790,6 +782,7 @@ fn render(self: *Game) !void { const allocator_general = ctx.allocator_general; const window = ctx.window; const swapchain = ctx.swapchain; + const gui = ctx.gui; const framebuffer_width, const framebuffer_height = blk: { const w, const h = window.getFramebufferSize(); @@ -1020,7 +1013,7 @@ fn render(self: *Game) !void { // --- RENDER GUI --- - try self.gui.draw(command_buffer); + try gui.draw(command_buffer); // --- FINALIZE --- diff --git a/src/Player.zig b/src/Player.zig index 34a1eb4..c4a2caf 100644 --- a/src/Player.zig +++ b/src/Player.zig @@ -262,12 +262,7 @@ pub fn onMouseDown(self: *Player, button: glfw.MouseButton, game: *Game) void { }, .right => blk: { const target_vx = raycast_hit.voxel.add(raycast_hit.side.getSignVector()); - const id = game.blocks.getOrLoad( - &game.materials, - &game.textures, - &game.stbi, - blocks[self.block_index], - ) catch |err| { + const id = game.blocks.getOrLoad(blocks[self.block_index]) catch |err| { std.log.err("Error while placing voxel at {f}: {}", .{ target_vx, err }); break :blk; }; diff --git a/src/assets/Blocks.zig b/src/assets/Blocks.zig index 30d46d3..f57c83b 100644 --- a/src/assets/Blocks.zig +++ b/src/assets/Blocks.zig @@ -136,13 +136,7 @@ pub fn getAtom(self: *const Blocks, filename: Atom) ?Id { }); } -pub fn getOrLoad( - self: *Blocks, - materials: *Materials, - textures: *Textures, - stbi: *media.stbi, - filename: []const u8, -) !Id { +pub fn getOrLoad(self: *Blocks, filename: []const u8) !Id { const key: Key = .{ // If the material already exists, then the atom must exist and the // following line will not return any error. @@ -158,7 +152,7 @@ pub fn getOrLoad( const id = Id.fromIndexSafe(self.array.items.len) catch |err| switch (err) { error.Overflow => return error.OutOfBlocks, }; - const def = try loadBlock(materials, textures, stbi, filename); + const def = try loadBlock(filename); self.map.putAssumeCapacityNoClobber(key, id); self.array.appendAssumeCapacity(def); @@ -167,13 +161,7 @@ pub fn getOrLoad( } } -pub fn getOrLoadAtom( - self: *Blocks, - materials: *Materials, - textures: *Textures, - stbi: *media.stbi, - filename: Atom, -) !Id { +pub fn getOrLoadAtom(self: *Blocks, filename: Atom) !Id { const key: Key = .{ .filename = filename, }; @@ -187,7 +175,7 @@ pub fn getOrLoadAtom( const id = Id.fromIndexSafe(self.array.items.len) catch |err| switch (err) { error.Overflow => return error.OutOfBlocks, }; - const def = try loadBlock(materials, textures, stbi, filename.toString()); + const def = try loadBlock(filename.toString()); self.map.putAssumeCapacityNoClobber(key, id); self.array.appendAssumeCapacity(def); @@ -196,12 +184,7 @@ pub fn getOrLoadAtom( } } -pub fn loadAll( - self: *Blocks, - materials: *Materials, - textures: *Textures, - stbi: *media.stbi, -) void { +pub fn loadAll(self: *Blocks) void { const io = ctx.io; const cwd = std.Io.Dir.cwd(); @@ -222,20 +205,16 @@ pub fn loadAll( continue; } - _ = self.getOrLoad(materials, textures, stbi, entry.name) catch |err| { + _ = self.getOrLoad(entry.name) catch |err| { std.log.err("Error while loading block definition entry {s}: {s}", .{ entry.name, @errorName(err) }); }; } } -fn loadBlock( - materials: *Materials, - textures: *Textures, - stbi: *media.stbi, - filename: []const u8, -) !Definition { +fn loadBlock(filename: []const u8) !Definition { const allocator_frame = ctx.allocator_frame; const io = ctx.io; + const materials = ctx.materials; const DefinitionJson = struct { pub const Wall = struct { @@ -278,7 +257,7 @@ fn loadBlock( return error.ParseError; } - const material = try materials.getOrLoad(textures, stbi, name); + const material = try materials.getOrLoad(name); break :blk .initUniform(material); } @@ -288,7 +267,7 @@ fn loadBlock( var ret: Definition.Walls = undefined; inline for (@typeInfo(voxels.Orientation).@"enum".fields) |field| { @field(ret, field.name) = .{ - .material = try materials.getOrLoad(textures, stbi, @field(walls, field.name).material), + .material = try materials.getOrLoad(@field(walls, field.name).material), .transform = @field(walls, field.name).transform, }; } diff --git a/src/engine/Atom.zig b/src/engine/Atom.zig index 7e17253..ff78efc 100644 --- a/src/engine/Atom.zig +++ b/src/engine/Atom.zig @@ -35,7 +35,6 @@ pub const Atom = enum(u16) { /// new one, if necessary. This will always produce a valid atom. Will not /// return any error if the atom already exists. pub fn fromString(string: []const u8) error{ OutOfMemory, OutOfAtoms }!Atom { - const allocator_general = ctx.allocator_general; const allocator_persistent = ctx.allocator_persistent; const io = ctx.io; const atoms = ctx.atoms; @@ -43,23 +42,21 @@ pub const Atom = enum(u16) { atoms.mutex.lockUncancelable(io); defer atoms.mutex.unlock(io); - const entry = try atoms.map.getOrPut(allocator_general, string); + // We don't use `getOrPutAssumeCapacity` method, because we might + // already be at full capacity, in which case we should return + // `error.OutOfAtoms`. - if (entry.found_existing) { - return entry.value_ptr.*; + if (atoms.map.get(string)) |atom| { + return atom; } else { - errdefer _ = atoms.map.remove(string); const atom = Atom.fromIndexSafe(atoms.array.items.len) catch |err| switch (err) { error.Overflow => return error.OutOfAtoms, }; - - try atoms.array.ensureUnusedCapacity(allocator_general, 1); const owned_string = try allocator_persistent.dupeZ(u8, string); - entry.key_ptr.* = owned_string; - entry.value_ptr.* = atom; - + atoms.map.putAssumeCapacityNoClobber(owned_string, atom); atoms.array.appendAssumeCapacity(owned_string); + return atom; } } diff --git a/src/engine/Atoms.zig b/src/engine/Atoms.zig index 8ad410c..76cce47 100644 --- a/src/engine/Atoms.zig +++ b/src/engine/Atoms.zig @@ -18,21 +18,20 @@ const ctx = @import("../AppContext.zig"); const Atom = @import("Atom.zig").Atom; -/// Maps a string value to an atom value. Uses `allocator_general`. +/// Maps a string value to an atom value. Preallocated with +/// `allocator_persistent`. map: std.StringHashMapUnmanaged(Atom), - -/// Maps an atom value to a string. Uses `allocator_general`. +/// Maps an atom value to a string. Preallocated with `allocator_persistent`. array: std.ArrayList([:0]const u8), - /// Protects all reads and writes to `map` and `array`. mutex: std.Io.Mutex, +pub const max_atoms = 1 << @typeInfo(std.meta.Tag(Atom)).int.bits; + pub fn init() !*Atoms { - const allocator_general = ctx.allocator_general; const allocator_persistent = ctx.allocator_persistent; const atoms = try allocator_persistent.create(Atoms); - errdefer allocator_persistent.destroy(atoms); atoms.* = .{ .map = .empty, @@ -40,26 +39,24 @@ pub fn init() !*Atoms { .mutex = .init, }; + try atoms.map.ensureTotalCapacity(allocator_persistent, max_atoms); + try atoms.array.ensureTotalCapacityPrecise(allocator_persistent, max_atoms); + // VOLATILE The initial contents of `atoms.map` and `atoms.array` must // correspond to explicitly defined values at the top of the `Atom` type. - try atoms.map.put(allocator_general, "", .empty); - try atoms.array.append(allocator_general, ""); + atoms.map.putAssumeCapacityNoClobber("", .empty); + atoms.array.appendAssumeCapacity(""); return atoms; } pub fn deinit(self: *Atoms) void { - const allocator_general = ctx.allocator_general; - std.log.scoped(.deinit).debug("Deinitializing {*}", .{self}); // No waiting; if atoms are in use while deinitializing, something is wrong. std.debug.assert(self.mutex.tryLock()); - self.map.deinit(allocator_general); - self.array.deinit(allocator_general); - self.* = undefined; } diff --git a/src/engine/DeviceAllocation.zig b/src/engine/DeviceAllocation.zig index eaaa2e6..4110ba0 100644 --- a/src/engine/DeviceAllocation.zig +++ b/src/engine/DeviceAllocation.zig @@ -9,6 +9,15 @@ memory_type_index: u32, allocated: usize, capacity: usize, +/// Dummy `DeviceAllocation`, which isn't safe to use, but it is safe to call +/// `deinit` on it (once). +pub const empty: DeviceAllocation = .{ + .device_memory = .null_handle, + .memory_type_index = vk.MAX_MEMORY_TYPES, + .allocated = 0, + .capacity = 0, +}; + pub fn deinit(self: *DeviceAllocation) void { const engine = ctx.engine; diff --git a/src/engine/Engine.zig b/src/engine/Engine.zig index 3f50294..e48803b 100644 --- a/src/engine/Engine.zig +++ b/src/engine/Engine.zig @@ -196,11 +196,9 @@ pub fn init() !*Engine { const physical_device_memory_properties = instance.getPhysicalDeviceMemoryProperties(physical_device); var memory_types = try std.ArrayList(vk.MemoryType).initCapacity(allocator_persistent, vk.MAX_MEMORY_TYPES); - errdefer memory_types.deinit(allocator_persistent); memory_types.appendSliceAssumeCapacity(physical_device_memory_properties.memory_types[0..physical_device_memory_properties.memory_type_count]); var memory_heaps = try std.ArrayList(vk.MemoryHeap).initCapacity(allocator_persistent, vk.MAX_MEMORY_HEAPS); - errdefer memory_heaps.deinit(allocator_persistent); memory_heaps.appendSliceAssumeCapacity(physical_device_memory_properties.memory_heaps[0..physical_device_memory_properties.memory_heap_count]); // --- CREATE SURFACE ------------------------------------------------------ diff --git a/src/Gui.zig b/src/engine/Gui.zig similarity index 87% rename from src/Gui.zig rename to src/engine/Gui.zig index d0268af..4e3b790 100644 --- a/src/Gui.zig +++ b/src/engine/Gui.zig @@ -1,17 +1,17 @@ const Gui = @This(); const std = @import("std"); -const ctx = @import("AppContext.zig"); -const shaders = @import("shaders.zig"); +const ctx = @import("../AppContext.zig"); +const shaders = @import("../shaders.zig"); const vk = @import("vulkan"); const vm = @import("vecmath"); -const CommandBuffer = @import("engine/CommandBuffer.zig"); -const Engine = @import("engine/Engine.zig"); -const GenericBuffer = @import("engine/GenericBuffer.zig").GenericBuffer; -const Swapchain = @import("engine/Swapchain.zig"); -const Texture = @import("engine/Texture.zig"); -const Textures = @import("engine/Textures.zig"); +const CommandBuffer = @import("CommandBuffer.zig"); +const Engine = @import("Engine.zig"); +const GenericBuffer = @import("GenericBuffer.zig").GenericBuffer; +const Swapchain = @import("Swapchain.zig"); +const Texture = @import("Texture.zig"); +const Textures = @import("Textures.zig"); pub const Draw = struct { pub const Box = extern struct { @@ -74,13 +74,13 @@ pub const max_text_draws = 4096; pub const max_batches = 1024; -pub fn init( - global_uniforms_buffer: vk.Buffer, -) !Gui { - const allocator_general = ctx.allocator_general; +pub fn init() !*Gui { + const allocator_persistent = ctx.allocator_persistent; const engine = ctx.engine; const swapchain = ctx.swapchain; + const gui = try allocator_persistent.create(Gui); + var box_gpu_buffer: GenericBuffer(void, Draw.Box) = try .init(.{ .usage = .storage, .target_queue = .graphics, @@ -105,17 +105,10 @@ pub fn init( }); errdefer image_gpu_buffer.deinit(); - var box_cpu_buffer: std.ArrayList(Draw.Box) = try .initCapacity(allocator_general, max_box_draws); - errdefer box_cpu_buffer.deinit(allocator_general); - - var text_cpu_buffer: std.ArrayList(Draw.Text) = try .initCapacity(allocator_general, max_text_draws); - errdefer text_cpu_buffer.deinit(allocator_general); - - var image_cpu_buffer: std.ArrayList(Draw.Image) = try .initCapacity(allocator_general, max_image_draws); - errdefer image_cpu_buffer.deinit(allocator_general); - - var batches: std.ArrayList(Batch) = try .initCapacity(allocator_general, max_batches); - errdefer batches.deinit(allocator_general); + const box_cpu_buffer: std.ArrayList(Draw.Box) = try .initCapacity(allocator_persistent, max_box_draws); + const text_cpu_buffer: std.ArrayList(Draw.Text) = try .initCapacity(allocator_persistent, max_text_draws); + const image_cpu_buffer: std.ArrayList(Draw.Image) = try .initCapacity(allocator_persistent, max_image_draws); + const batches: std.ArrayList(Batch) = try .initCapacity(allocator_persistent, max_batches); const sampler = try engine.createSampler(.{ .mag_filter = .linear, @@ -308,21 +301,6 @@ pub fn init( try engine.updateDescriptorSets(.{ .writes = &.{ - .{ - .dst_set = box_descriptor_set, - .dst_binding = 0, - .dst_array_element = 0, - .descriptor_type = .uniform_buffer, - .descriptor_infos = .{ - .buffer = &.{ - .{ - .buffer = global_uniforms_buffer, - .offset = 0, - .range = vk.WHOLE_SIZE, - }, - }, - }, - }, .{ .dst_set = box_descriptor_set, .dst_binding = 1, @@ -341,7 +319,7 @@ pub fn init( }, }); - return .{ + gui.* = .{ .box_gpu_buffer = box_gpu_buffer, .text_gpu_buffer = text_gpu_buffer, .image_gpu_buffer = image_gpu_buffer, @@ -362,10 +340,11 @@ pub fn init( .descriptor_pool = descriptor_pool, .box_descriptor_set = box_descriptor_set, }; + + return gui; } pub fn deinit(self: *Gui) void { - const allocator_general = ctx.allocator_general; const engine = ctx.engine; std.log.scoped(.deinit).debug("Deinitializing {*}", .{self}); @@ -377,12 +356,6 @@ pub fn deinit(self: *Gui) void { engine.destroyDescriptorSetLayout(self.box_descriptor_set_layout); engine.destroySampler(self.sampler); - self.batches.deinit(allocator_general); - - self.image_cpu_buffer.deinit(allocator_general); - self.text_cpu_buffer.deinit(allocator_general); - self.box_cpu_buffer.deinit(allocator_general); - self.image_gpu_buffer.deinit(); self.text_gpu_buffer.deinit(); self.box_gpu_buffer.deinit(); diff --git a/src/engine/Materials.zig b/src/engine/Materials.zig index 8d53b5c..f81a7fe 100644 --- a/src/engine/Materials.zig +++ b/src/engine/Materials.zig @@ -58,7 +58,7 @@ pub const Key = struct { filename: Atom, }; -/// Maps a key value to a material ID. Preallocated with `allocator_general`. +/// Maps a key value to a material ID. Preallocated with `allocator_persistent`. map: std.AutoHashMapUnmanaged(Key, Id), /// Stores all material data in a single contiguous storage buffer. Use the /// material ID as an index into this buffer. @@ -70,24 +70,30 @@ material_count: usize, /// storage buffer should take 208 kiB in VRAM. pub const max_materials = 4096; -pub fn init() !Materials { - const allocator_general = ctx.allocator_general; +pub fn init() !*Materials { + const allocator_persistent = ctx.allocator_persistent; - var map: std.AutoHashMapUnmanaged(Key, Id) = .empty; - errdefer map.deinit(allocator_general); - try map.ensureTotalCapacity(allocator_general, max_materials); + const materials = try allocator_persistent.create(Materials); - var material_buffer = try shaders.MaterialBuffer.init(.{ + materials.* = .{ + .map = .empty, + .material_buffer = undefined, + .material_count = 0, + }; + + try materials.map.ensureTotalCapacity(allocator_persistent, max_materials); + + materials.material_buffer = try .init(.{ .usage = .storage, .target_queue = .graphics, .array_capacity = max_materials, .name = "Materials", }); - errdefer material_buffer.deinit(); + errdefer materials.material_buffer.deinit(); // VOLATILE Synchronize with explicit values on top of `Id` type. - try material_buffer.write(.{ + try materials.material_buffer.write(.{ .element_offset = Id.empty.toInt(), .elements = &.{ .{ @@ -105,21 +111,16 @@ pub fn init() !Materials { }, }, }); + materials.material_count += 1; - return .{ - .map = map, - .material_buffer = material_buffer, - .material_count = @typeInfo(Id).@"enum".fields.len, - }; + std.debug.assert(materials.material_count == @typeInfo(Id).@"enum".fields.len); + return materials; } pub fn deinit(self: *Materials) void { - const allocator_general = ctx.allocator_general; - std.log.scoped(.deinit).debug("Deinitializing {*}", .{self}); self.material_buffer.deinit(); - self.map.deinit(allocator_general); self.* = undefined; } @@ -161,12 +162,7 @@ pub fn getAtom(self: *const Materials, filename: Atom) ?Id { /// deinitialized or reset after this function returns. Note that during loading /// the engine will make its own persistent allocations, so an out of memory /// error is not necessarily related to `temp_allocator`. -pub fn getOrLoad( - self: *Materials, - textures: *Textures, - stbi: *media.stbi, - maybe_filename: ?[]const u8, -) !Id { +pub fn getOrLoad(self: *Materials, maybe_filename: ?[]const u8) !Id { if (maybe_filename) |filename| { const key: Key = .{ // If the material already exists, then the atom must exist and the @@ -183,7 +179,7 @@ pub fn getOrLoad( const id = Id.fromIndexSafe(self.material_count) catch |err| switch (err) { error.Overflow => return error.OutOfMaterials, }; - try self.loadMaterial(textures, stbi, filename, id.toInt()); + try self.loadMaterial(filename, id.toInt()); self.map.putAssumeCapacityNoClobber(key, id); self.material_count += 1; @@ -206,12 +202,7 @@ pub fn getOrLoad( /// deinitialized or reset after this function returns. Note that during loading /// the engine will make its own persistent allocations, so an out of memory /// error is not necessarily related to `temp_allocator`. -pub fn getOrLoadAtom( - self: *Materials, - textures: *Textures, - stbi: *media.stbi, - filename: Atom, -) !Id { +pub fn getOrLoadAtom(self: *Materials, filename: Atom) !Id { if (filename != .empty) { const key: Key = .{ .filename = filename, @@ -226,7 +217,7 @@ pub fn getOrLoadAtom( const id = Id.fromIndexSafe(self.material_count) catch |err| switch (err) { error.Overflow => return error.OutOfMaterials, }; - try self.loadMaterial(textures, stbi, filename.toString(), id.toInt()); + try self.loadMaterial(filename.toString(), id.toInt()); self.map.putAssumeCapacityNoClobber(key, id); self.material_count += 1; @@ -248,11 +239,7 @@ pub fn getOrLoadAtom( /// deinitialized or reset after this function returns. Note that during loading /// the engine will make its own persistent allocations, so an out of memory /// error is not necessarily related to `temp_allocator`. -pub fn loadAll( - self: *Materials, - textures: *Textures, - stbi: *media.stbi, -) void { +pub fn loadAll(self: *Materials) void { const io = ctx.io; const cwd = std.Io.Dir.cwd(); @@ -273,15 +260,16 @@ pub fn loadAll( continue; } - _ = self.getOrLoad(textures, stbi, entry.name) catch |err| { + _ = self.getOrLoad(entry.name) catch |err| { std.log.err("Error while loading material entry {s}: {s}", .{ entry.name, @errorName(err) }); }; } } -fn loadMaterial(self: *Materials, textures: *Textures, stbi: *media.stbi, filename: []const u8, index: u32) !void { +fn loadMaterial(self: *Materials, filename: []const u8, index: u32) !void { const allocator_frame = ctx.allocator_frame; const io = ctx.io; + const textures = ctx.textures; const MaterialJson = struct { baseColor: [3]f32 = .{ 1, 1, 1 }, @@ -330,10 +318,10 @@ fn loadMaterial(self: *Materials, textures: *Textures, stbi: *media.stbi, filena .normal_scale = material_json.normalScale, .occlusion_texture_strength = material_json.occlusionTextureStrength, .roughness = material_json.roughness, - .base_color_texture = try textures.getOrLoad(stbi, material_json.baseColorTexture, .base_color), - .emissive_texture = try textures.getOrLoad(stbi, material_json.emissiveTexture, .emissive), - .normal_texture = try textures.getOrLoad(stbi, material_json.normalTexture, .normal), - .occlusion_roughness_metallic_texture = try textures.getOrLoad(stbi, material_json.occlusionRoughnessMetallicTexture, .occlusion_roughness_metallic), + .base_color_texture = try textures.getOrLoad(material_json.baseColorTexture, .base_color), + .emissive_texture = try textures.getOrLoad(material_json.emissiveTexture, .emissive), + .normal_texture = try textures.getOrLoad(material_json.normalTexture, .normal), + .occlusion_roughness_metallic_texture = try textures.getOrLoad(material_json.occlusionRoughnessMetallicTexture, .occlusion_roughness_metallic), }, }, }); diff --git a/src/engine/Skybox.zig b/src/engine/Skybox.zig index ef72cc3..f0df16d 100644 --- a/src/engine/Skybox.zig +++ b/src/engine/Skybox.zig @@ -30,12 +30,12 @@ pipeline: vk.Pipeline, pub fn load( filename: []const u8, - stbi: *media.stbi, cube_size: u32, global_uniforms_buffer: vk.Buffer, ) !Skybox { const allocator_frame = ctx.allocator_frame; const io = ctx.io; + const stbi = ctx.stbi; const engine = ctx.engine; const swapchain = ctx.swapchain; diff --git a/src/engine/Textures.zig b/src/engine/Textures.zig index bd4e91d..c59923b 100644 --- a/src/engine/Textures.zig +++ b/src/engine/Textures.zig @@ -59,10 +59,10 @@ pub const Key = struct { usage: Texture.Usage, }; -/// Maps a key value to a texture ID. Preallocated with `allocator_general`. +/// Maps a key value to a texture ID. Preallocated with `allocator_persistent`. map: std.AutoHashMapUnmanaged(Key, Id), /// Stores all `Texture` structs and maps a texture ID to a `Texture` struct. -/// Preallocated with `allocator_general`. +/// Preallocated with `allocator_persistent`. array: std.ArrayList(Texture), device_allocation: DeviceAllocation, @@ -70,23 +70,28 @@ pub const max_textures = 4096; /// Enough for 4096 textures of usage `.base_color` and 64×64 dimensions. pub const max_memory = 64 * 1024 * 1024; -pub fn init() !Textures { - const allocator_general = ctx.allocator_general; +pub fn init() !*Textures { + const allocator_persistent = ctx.allocator_persistent; const engine = ctx.engine; - var map: std.AutoHashMapUnmanaged(Key, Id) = .empty; - errdefer map.deinit(allocator_general); - try map.ensureTotalCapacity(allocator_general, max_textures); + const textures = try allocator_persistent.create(Textures); + + textures.* = .{ + .map = .empty, + .array = .empty, + .device_allocation = .empty, + }; - var array: std.ArrayList(Texture) = try .initCapacity(allocator_general, max_textures); errdefer { - for (array.items) |*texture| { + for (textures.array.items) |*texture| { texture.deinit(); } - array.deinit(allocator_general); + textures.device_allocation.deinit(); } - var device_allocation = try engine.allocateForImage( + try textures.map.ensureTotalCapacity(allocator_persistent, max_textures); + try textures.array.ensureTotalCapacityPrecise(allocator_persistent, max_textures); + textures.device_allocation = try engine.allocateForImage( .{ .image_type = .@"2d", .format = .r8g8b8a8_unorm, @@ -113,7 +118,6 @@ pub fn init() !Textures { max_memory, .{ .device_local_bit = true }, ); - errdefer engine.freeMemory(device_allocation.device_memory); // VOLATILE Synchronize with explicit values on top of `Id` type. @@ -122,64 +126,56 @@ pub fn init() !Textures { .height = 1, .usage = .base_color, .target_queue = .graphics, - .device_allocation = &device_allocation, + .device_allocation = &textures.device_allocation, .name = "@Empty", }); - array.appendAssumeCapacity(empty_base_color_texture); + textures.array.appendAssumeCapacity(empty_base_color_texture); const empty_emissive_texture = try Texture.init(.{ .width = 1, .height = 1, .usage = .emissive, .target_queue = .graphics, - .device_allocation = &device_allocation, + .device_allocation = &textures.device_allocation, .name = "@Empty", }); - array.appendAssumeCapacity(empty_emissive_texture); + textures.array.appendAssumeCapacity(empty_emissive_texture); const empty_normal_texture = try Texture.init(.{ .width = 1, .height = 1, .usage = .normal, .target_queue = .graphics, - .device_allocation = &device_allocation, + .device_allocation = &textures.device_allocation, .name = "@Empty", }); - array.appendAssumeCapacity(empty_normal_texture); + textures.array.appendAssumeCapacity(empty_normal_texture); const empty_occlusuion_roughness_metallic_texture = try Texture.init(.{ .width = 1, .height = 1, .usage = .occlusion_roughness_metallic, .target_queue = .graphics, - .device_allocation = &device_allocation, + .device_allocation = &textures.device_allocation, .name = "@Empty", }); - array.appendAssumeCapacity(empty_occlusuion_roughness_metallic_texture); + textures.array.appendAssumeCapacity(empty_occlusuion_roughness_metallic_texture); try empty_base_color_texture.writeSamples(u8, &.{ 255, 255, 255, 255 }); try empty_emissive_texture.writeSamples(f16, &.{ 1.0, 1.0, 1.0, 1.0 }); try empty_normal_texture.writeSamples(i8, &.{ 0, 0, 127, 127 }); try empty_occlusuion_roughness_metallic_texture.writeSamples(u8, &.{ 255, 255, 255, 255 }); - return .{ - .map = map, - .array = array, - .device_allocation = device_allocation, - }; + return textures; } pub fn deinit(self: *Textures) void { - const allocator_general = ctx.allocator_general; - std.log.scoped(.deinit).debug("Deinitializing {*}", .{self}); for (self.array.items) |*texture| { texture.deinit(); } self.device_allocation.deinit(); - self.array.deinit(allocator_general); - self.map.deinit(allocator_general); self.* = undefined; } @@ -217,12 +213,7 @@ pub fn getAtom(self: *const Textures, filename: Atom, usage: Texture.Usage) ?Id /// if necessary. Will not return any error if the texture already exists. When /// the filename is `null`, returns an empty texture ID appropriate for given /// usage. -pub fn getOrLoad( - self: *Textures, - stbi: *media.stbi, - maybe_filename: ?[]const u8, - usage: Texture.Usage, -) !Id { +pub fn getOrLoad(self: *Textures, maybe_filename: ?[]const u8, usage: Texture.Usage) !Id { if (maybe_filename) |filename| { const key: Key = .{ // If the texture already exists, then the atom must exist and the @@ -240,7 +231,7 @@ pub fn getOrLoad( const id = Id.fromIndexSafe(self.array.items.len) catch |err| switch (err) { error.Overflow => return error.OutOfTextures, }; - const texture = try loadTexture(stbi, filename, usage, &self.device_allocation); + const texture = try loadTexture(filename, usage, &self.device_allocation); self.map.putAssumeCapacityNoClobber(key, id); self.array.appendAssumeCapacity(texture); @@ -257,20 +248,16 @@ pub fn getOrLoad( /// if necessary. Will not return any error if the texture already exists. When /// the filename is `.empty`, returns an empty texture ID appropriate for given /// usage. -pub fn getOrLoadAtom( - self: *Textures, - stbi: *media.stbi, - filename: Atom, - usage: Texture.Usage, -) !Id { +pub fn getOrLoadAtom(self: *Textures, filename: Atom, usage: Texture.Usage) !Id { if (filename != .empty) { const key: Key = .{ .filename = filename, .usage = usage, }; - // We don't use `getOrPutAssumeCapacity` method, because we might already be - // at full capacity, in which case we should return `error.OutOfTextures`. + // We don't use `getOrPutAssumeCapacity` method, because we might + // already be at full capacity, in which case we should return + // `error.OutOfTextures`. if (self.map.get(key)) |id| { return id; @@ -278,7 +265,7 @@ pub fn getOrLoadAtom( const id = Id.fromIndexSafe(self.array.items.len) catch |err| switch (err) { error.Overflow => return error.OutOfTextures, }; - const texture = try loadTexture(stbi, filename.toString(), usage, &self.device_allocation); + const texture = try loadTexture(filename.toString(), usage, &self.device_allocation); self.map.putAssumeCapacityNoClobber(key, id); self.array.appendAssumeCapacity(texture); @@ -291,12 +278,12 @@ pub fn getOrLoadAtom( } fn loadTexture( - stbi: *media.stbi, filename: []const u8, usage: Texture.Usage, device_allocation: *DeviceAllocation, ) !Texture { const io = ctx.io; + const stbi = ctx.stbi; std.log.debug("Loading texture \"{s}\" as {s}...", .{ filename, @tagName(usage) }); diff --git a/src/main.zig b/src/main.zig index 6233382..a168216 100644 --- a/src/main.zig +++ b/src/main.zig @@ -3,6 +3,7 @@ const std = @import("std"); const c = @import("const.zig"); const ctx = @import("AppContext.zig"); const glfw = @import("zglfw"); +const media = @import("media"); const vk = @import("vulkan"); const Atoms = @import("engine/Atoms.zig"); @@ -37,6 +38,9 @@ pub fn main(init: std.process.Init) !void { ctx.vk_allocator.deinit(); ctx.vk_allocator = undefined; } + ctx.stbi = try ctx.allocator_persistent.create(media.stbi); + ctx.stbi.* = .init(ctx.allocator_general, ctx.io); + defer ctx.stbi.deinit(); // --- INITIALIZE APP CONTEXT --- @@ -89,6 +93,24 @@ pub fn main(init: std.process.Init) !void { ctx.swapchain = undefined; } + ctx.textures = try .init(); + defer { + ctx.textures.deinit(); + ctx.textures = undefined; + } + + ctx.materials = try .init(); + defer { + ctx.materials.deinit(); + ctx.materials = undefined; + } + + ctx.gui = try .init(); + defer { + ctx.gui.deinit(); + ctx.gui = undefined; + } + // --- INITIALIZE THE GAME --- var game = try Game.init(); @@ -117,6 +139,7 @@ pub fn main(init: std.process.Init) !void { const dt: f32 = @floatCast(t2 - t1); t1 = t2; + ctx.gui.beginFrame(); game.update(dt); _ = arena_frame.reset(.retain_capacity); }