diff --git a/src/assets/Chunk.zig b/src/Chunk.zig similarity index 95% rename from src/assets/Chunk.zig rename to src/Chunk.zig index 58e6ba7..972fbf0 100644 --- a/src/assets/Chunk.zig +++ b/src/Chunk.zig @@ -1,19 +1,19 @@ const Chunk = @This(); const std = @import("std"); -const ctx = @import("../AppContext.zig"); -const math = @import("../math.zig"); -const shaders = @import("../shaders.zig"); +const ctx = @import("AppContext.zig"); +const math = @import("math.zig"); +const shaders = @import("shaders.zig"); const vk = @import("vulkan"); const vm = @import("vecmath"); -const voxels = @import("../voxels.zig"); +const voxels = @import("voxels.zig"); -const Blocks = @import("Blocks.zig"); -const CommandBuffer = @import("../engine/CommandBuffer.zig"); -const Engine = @import("../engine/Engine.zig"); -const Game = @import("../Game.zig"); -const GenericBuffer = @import("../engine/GenericBuffer.zig").GenericBuffer; -const Materials = @import("Materials.zig"); +const Blocks = @import("assets/Blocks.zig"); +const CommandBuffer = @import("engine/CommandBuffer.zig"); +const Engine = @import("engine/Engine.zig"); +const Game = @import("Game.zig"); +const GenericBuffer = @import("engine/GenericBuffer.zig").GenericBuffer; +const Materials = @import("engine/Materials.zig"); const ObjectUniformsBuffer = GenericBuffer(void, shaders.ObjectUniforms); diff --git a/src/Chunks.zig b/src/Chunks.zig index 4986ba2..0a7c589 100644 --- a/src/Chunks.zig +++ b/src/Chunks.zig @@ -9,7 +9,7 @@ const vm = @import("vecmath"); const voxels = @import("voxels.zig"); const Blocks = @import("assets/Blocks.zig"); -const Chunk = @import("assets/Chunk.zig"); +const Chunk = @import("Chunk.zig"); const Engine = @import("engine/Engine.zig"); const Iterator2 = math.Iterator2; diff --git a/src/Game.zig b/src/Game.zig index 9c6fe77..06b8f53 100644 --- a/src/Game.zig +++ b/src/Game.zig @@ -12,19 +12,19 @@ const vm = @import("vecmath"); const worldgen = @import("worldgen.zig"); const Blocks = @import("assets/Blocks.zig"); -const Chunk = @import("assets/Chunk.zig"); +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("assets/Materials.zig"); +const Materials = @import("engine/Materials.zig"); const Player = @import("Player.zig"); const Skybox = @import("engine/Skybox.zig"); const StagingBuffer = @import("engine/StagingBuffer.zig"); const Swapchain = @import("engine/Swapchain.zig"); const Texture = @import("engine/Texture.zig"); -const Textures = @import("assets/Textures.zig"); +const Textures = @import("engine/Textures.zig"); global_descriptor_set_layout: vk.DescriptorSetLayout, per_batch_descriptor_set_layout: vk.DescriptorSetLayout, @@ -989,24 +989,24 @@ fn render(self: *Game) !void { }); command_buffer.setViewport(0, .{ - .x = 0, - .y = 0, - .width = @floatFromInt(extent.width), - .height = @floatFromInt(extent.height), - .min_depth = 0, - .max_depth = 1, + .x = 0, + .y = 0, + .width = @floatFromInt(extent.width), + .height = @floatFromInt(extent.height), + .min_depth = 0, + .max_depth = 1, }); command_buffer.setScissor(0, .{ - .offset = .{ .x = 0, .y = 0 }, - .extent = extent, + .offset = .{ .x = 0, .y = 0 }, + .extent = extent, }); // --- RENDER 3D SCENE --- command_buffer.bindPipeline(.graphics, self.pipeline); try command_buffer.bindVertexBuffer(0, .{ - .buffer = self.vertex_buffer.buffer, - .offset = 0, + .buffer = self.vertex_buffer.buffer, + .offset = 0, }); command_buffer.bindIndexBuffer(self.index_buffer.buffer, 0, .uint16); command_buffer.bindDescriptorSet(.graphics, self.pipeline_layout, 0, self.global_descriptor_set, null); diff --git a/src/Gui.zig b/src/Gui.zig index ec68a87..d0268af 100644 --- a/src/Gui.zig +++ b/src/Gui.zig @@ -11,7 +11,7 @@ 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("assets/Textures.zig"); +const Textures = @import("engine/Textures.zig"); pub const Draw = struct { pub const Box = extern struct { diff --git a/src/assets/Blocks.zig b/src/assets/Blocks.zig index 1154500..30d46d3 100644 --- a/src/assets/Blocks.zig +++ b/src/assets/Blocks.zig @@ -9,8 +9,8 @@ const voxels = @import("../voxels.zig"); const Atom = @import("../engine/Atom.zig").Atom; const Engine = @import("../engine/Engine.zig"); -const Materials = @import("Materials.zig"); -const Textures = @import("Textures.zig"); +const Materials = @import("../engine/Materials.zig"); +const Textures = @import("../engine/Textures.zig"); pub const Id = enum(u16) { // VOLATILE Synchronize explicit values with `init` implementation. diff --git a/src/engine/DeviceAllocation.zig b/src/engine/DeviceAllocation.zig new file mode 100644 index 0000000..eaaa2e6 --- /dev/null +++ b/src/engine/DeviceAllocation.zig @@ -0,0 +1,53 @@ +const std = @import("std"); +const DeviceAllocation = @This(); + +const ctx = @import("../AppContext.zig"); +const vk = @import("vulkan"); + +device_memory: vk.DeviceMemory, +memory_type_index: u32, +allocated: usize, +capacity: usize, + +pub fn deinit(self: *DeviceAllocation) void { + const engine = ctx.engine; + + engine.freeMemory(self.device_memory); + self.* = undefined; +} + +pub fn bindBuffer(self: *DeviceAllocation, buffer: vk.Buffer) !void { + const engine = ctx.engine; + + const memory_requirements = engine.getBufferMemoryRequirements(buffer); + const is_type = memory_requirements.memory_type_bits & (@as(u32, 1) << @truncate(self.memory_type_index)) != 0; + std.debug.assert(is_type); + + const offset = std.mem.alignForward(usize, self.allocated, memory_requirements.alignment); + const next_allocated = offset + memory_requirements.size; + + if (next_allocated > self.capacity) { + return error.OutOfMemory; + } + + try engine.bindBufferMemory(buffer, self.device_memory, offset); + self.allocated = next_allocated; +} + +pub fn bindImage(self: *DeviceAllocation, image: vk.Image) !void { + const engine = ctx.engine; + + const memory_requirements = engine.getImageMemoryRequirements(image); + const is_type = memory_requirements.memory_type_bits & (@as(u32, 1) << @truncate(self.memory_type_index)) != 0; + std.debug.assert(is_type); + + const offset = std.mem.alignForward(usize, self.allocated, memory_requirements.alignment); + const next_allocated = offset + memory_requirements.size; + + if (next_allocated > self.capacity) { + return error.OutOfMemory; + } + + try engine.bindImageMemory(image, self.device_memory, offset); + self.allocated = next_allocated; +} diff --git a/src/engine/Engine.zig b/src/engine/Engine.zig index 0cc5d91..3f50294 100644 --- a/src/engine/Engine.zig +++ b/src/engine/Engine.zig @@ -6,6 +6,7 @@ const ctx = @import("../AppContext.zig"); const glfw = @import("zglfw"); const vk = @import("vulkan"); +const DeviceAllocation = @import("DeviceAllocation.zig"); const Queue = @import("Queue.zig"); const QueueType = @import("QueueType.zig").QueueType; const VkAllocator = @import("VkAllocator.zig"); @@ -418,18 +419,46 @@ pub fn deinit(self: *Engine) void { } pub fn allocate(self: *const Engine, memory_requirements: vk.MemoryRequirements, memory_property_flags: vk.MemoryPropertyFlags) !vk.DeviceMemory { - for (self.memory_types.items, 0..) |memory_type, i| { - const is_type = memory_requirements.memory_type_bits & (@as(u32, 1) << @truncate(i)) != 0; - const has_flags = memory_type.property_flags.contains(memory_property_flags); - if (is_type and has_flags) { - return try self.device.allocateMemory(&.{ - .allocation_size = memory_requirements.size, - .memory_type_index = @truncate(i), - }, ctx.vk_allocator.capture()); - } - } + const memory_type_index = try self.findMemoryTypeIndex(memory_requirements.memory_type_bits, memory_property_flags); - return error.NoSuitableMemoryType; + return self.device.allocateMemory(&.{ + .allocation_size = memory_requirements.size, + .memory_type_index = memory_type_index, + }, ctx.vk_allocator.capture()); +} + +/// Allocate `vk.DeviceMemory` suitable for image created by given +/// `create_info`, except the allocation is `size` bytes large. +pub fn allocateForImage( + self: *const Engine, + create_info: ImageCreateInfo, + plane_aspect: vk.ImageAspectFlags, + size: usize, + memory_property_flags: vk.MemoryPropertyFlags, +) !DeviceAllocation { + const create_info_vk = try create_info.toVk(); + + var memory_requirements2: vk.MemoryRequirements2 = .{ + .memory_requirements = undefined, + }; + self.device.getDeviceImageMemoryRequirements(&.{ + .p_create_info = &create_info_vk, + .plane_aspect = plane_aspect, + }, &memory_requirements2); + + const memory_requirements = memory_requirements2.memory_requirements; + const memory_type_index = try self.findMemoryTypeIndex(memory_requirements.memory_type_bits, memory_property_flags); + const device_memory = try self.device.allocateMemory(&.{ + .allocation_size = size, + .memory_type_index = memory_type_index, + }, ctx.vk_allocator.capture()); + + return .{ + .device_memory = device_memory, + .allocated = 0, + .capacity = size, + .memory_type_index = memory_type_index, + }; } pub fn setObjectName(self: *const Engine, handle: anytype, comptime fmt: []const u8, args: anytype) void { @@ -663,8 +692,16 @@ fn findPresentationQueueFamily(queue_families_properties: []const vk.QueueFamily return null; } -fn makeArena(self: *const Engine) std.heap.ArenaAllocator { - return .init(self.vk_allocator.allocator); +fn findMemoryTypeIndex(self: *const Engine, memory_type_bits: u32, memory_property_flags: vk.MemoryPropertyFlags) !u32 { + for (self.memory_types.items, 0..) |memory_type, i| { + const is_type = memory_type_bits & (@as(u32, 1) << @truncate(i)) != 0; + const has_flags = memory_type.property_flags.contains(memory_property_flags); + if (is_type and has_flags) { + return @truncate(i); + } + } + + return error.NoSuitableMemoryType; } fn resolveCommandPool(self: *const Engine, queue_type: QueueType) vk.CommandPool { @@ -769,6 +806,33 @@ pub const ImageCreateInfo = struct { usage: vk.ImageUsageFlags, queue_family_indices: []const u32 = &.{}, initial_layout: vk.ImageLayout, + + pub fn toVk(self: ImageCreateInfo) !vk.ImageCreateInfo { + const allocator_frame = ctx.allocator_frame; + + var queue_family_indices_set: std.AutoArrayHashMapUnmanaged(u32, void) = .{}; + for (self.queue_family_indices) |queue_family_index| { + try queue_family_indices_set.put(allocator_frame, queue_family_index, {}); + } + + const queue_family_indices = queue_family_indices_set.keys(); + + return .{ + .flags = self.flags, + .image_type = self.image_type, + .format = self.format, + .extent = self.extent, + .mip_levels = self.mip_levels, + .array_layers = self.array_layers, + .samples = self.samples, + .tiling = self.tiling, + .usage = self.usage, + .sharing_mode = if (queue_family_indices.len > 1) .concurrent else .exclusive, + .queue_family_index_count = if (queue_family_indices.len > 1) @intCast(queue_family_indices.len) else 0, + .p_queue_family_indices = if (queue_family_indices.len > 1) queue_family_indices.ptr else null, + .initial_layout = self.initial_layout, + }; + } }; pub const ImageViewCreateInfo = struct { @@ -1110,31 +1174,8 @@ pub fn createGraphicsPipeline(self: *Engine, create_info: GraphicsPipelineCreate } pub fn createImage(self: *Engine, create_info: ImageCreateInfo) !vk.Image { - const allocator_frame = ctx.allocator_frame; - - var queue_family_indices_set: std.AutoArrayHashMapUnmanaged(u32, void) = .{}; - for (create_info.queue_family_indices) |queue_family_index| { - try queue_family_indices_set.put(allocator_frame, queue_family_index, {}); - } - - const queue_family_indices = queue_family_indices_set.keys(); - - const image = self.device.createImage(&.{ - .flags = create_info.flags, - .image_type = create_info.image_type, - .format = create_info.format, - .extent = create_info.extent, - .mip_levels = create_info.mip_levels, - .array_layers = create_info.array_layers, - .samples = create_info.samples, - .tiling = create_info.tiling, - .usage = create_info.usage, - .sharing_mode = if (queue_family_indices.len > 1) .concurrent else .exclusive, - .queue_family_index_count = if (queue_family_indices.len > 1) @intCast(queue_family_indices.len) else 0, - .p_queue_family_indices = if (queue_family_indices.len > 1) queue_family_indices.ptr else null, - .initial_layout = create_info.initial_layout, - }, ctx.vk_allocator.capture()); - return image; + const create_info_vk = try create_info.toVk(); + return self.device.createImage(&create_info_vk, ctx.vk_allocator.capture()); } pub fn createImageView(self: *Engine, create_info: ImageViewCreateInfo) !vk.ImageView { diff --git a/src/assets/Materials.zig b/src/engine/Materials.zig similarity index 95% rename from src/assets/Materials.zig rename to src/engine/Materials.zig index 861e3dd..8d53b5c 100644 --- a/src/assets/Materials.zig +++ b/src/engine/Materials.zig @@ -1,4 +1,9 @@ -//! Module for loading persistent materials. +//! Module for loading persistent materials, which are all stored in a single +//! storage buffer in VRAM. +//! +//! This module is intended to be initialized once and to persist until the end +//! of the whole program's runtime. Trying to use it in any other way will +//! result in weird behavior. const Materials = @This(); const std = @import("std"); @@ -13,11 +18,13 @@ const Engine = @import("../engine/Engine.zig"); const Textures = @import("Textures.zig"); pub const Id = enum(u16) { - // VOLATILE Synchronize explicit values with `init` implementation. + // VOLATILE When modifying the list of explicitly defined material IDs (i.e. + // any explicit enum value), we need to update `Materials.init` + // implementation. /// A material ID that can be used as a "null" material. An object with this - /// material ID must not be rendered. If this ID is rendered, an "error" - /// replacement material will be used. + /// material ID must not be rendered. If this material is rendered anyway, + /// an appropriate "error"-looking material will be used. empty, _, diff --git a/src/engine/Texture.zig b/src/engine/Texture.zig index 4793c90..b71e3c2 100644 --- a/src/engine/Texture.zig +++ b/src/engine/Texture.zig @@ -5,6 +5,7 @@ const ctx = @import("../AppContext.zig"); const vk = @import("vulkan"); const CommandBuffer = @import("CommandBuffer.zig"); +const DeviceAllocation = @import("DeviceAllocation.zig"); const Engine = @import("Engine.zig"); const StagingBuffer = @import("StagingBuffer.zig"); const TargetQueue = @import("TargetQueue.zig").TargetQueue; @@ -68,11 +69,14 @@ pub const InitInfo = struct { height: u32, usage: Usage, target_queue: TargetQueue, + device_allocation: ?*DeviceAllocation = null, name: ?[]const u8 = null, }; image: vk.Image, image_view: vk.ImageView, +/// Is `.null_handle` when external device memory is provided via +/// `device_allocation` in init info. device_memory: vk.DeviceMemory, target_queue: TargetQueue, @@ -114,14 +118,20 @@ pub fn init(init_info: InitInfo) !Texture { engine.setObjectName(image, "I {s} [{s}]", .{ name, @tagName(init_info.usage) }); } - const memory_requirements = engine.getImageMemoryRequirements(image); - const device_memory = try engine.allocate(memory_requirements, .{ .device_local_bit = true }); + var device_memory: vk.DeviceMemory = .null_handle; errdefer engine.freeMemory(device_memory); - if (init_info.name) |name| { - engine.setObjectName(device_memory, "DM {s} [{s}]", .{ name, @tagName(init_info.usage) }); - } - try engine.bindImageMemory(image, device_memory, 0); + if (init_info.device_allocation) |device_allocation| { + try device_allocation.bindImage(image); + } else { + const memory_requirements = engine.getImageMemoryRequirements(image); + device_memory = try engine.allocate(memory_requirements, .{ .device_local_bit = true }); + if (init_info.name) |name| { + engine.setObjectName(device_memory, "DM {s} [{s}]", .{ name, @tagName(init_info.usage) }); + } + + try engine.bindImageMemory(image, device_memory, 0); + } const image_view = try engine.createImageView(.{ .image = image, diff --git a/src/assets/Textures.zig b/src/engine/Textures.zig similarity index 85% rename from src/assets/Textures.zig rename to src/engine/Textures.zig index ad41979..bd4e91d 100644 --- a/src/assets/Textures.zig +++ b/src/engine/Textures.zig @@ -5,10 +5,12 @@ const std = @import("std"); const ctx = @import("../AppContext.zig"); const media = @import("media"); +const vk = @import("vulkan"); -const Atom = @import("../engine/Atom.zig").Atom; -const Engine = @import("../engine/Engine.zig"); -const Texture = @import("../engine/Texture.zig"); +const Atom = @import("Atom.zig").Atom; +const DeviceAllocation = @import("DeviceAllocation.zig"); +const Engine = @import("Engine.zig"); +const Texture = @import("Texture.zig"); pub const Id = enum(u16) { // VOLATILE Synchronize explicit values with `init` implementation. @@ -62,13 +64,15 @@ map: std.AutoHashMapUnmanaged(Key, Id), /// Stores all `Texture` structs and maps a texture ID to a `Texture` struct. /// Preallocated with `allocator_general`. array: std.ArrayList(Texture), +device_allocation: DeviceAllocation, -/// 4096 textures of usage `.base_color` and 16×16 dimensions should take 4 MiB -/// in VRAM. 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; + const engine = ctx.engine; var map: std.AutoHashMapUnmanaged(Key, Id) = .empty; errdefer map.deinit(allocator_general); @@ -82,6 +86,35 @@ pub fn init() !Textures { array.deinit(allocator_general); } + var device_allocation = try engine.allocateForImage( + .{ + .image_type = .@"2d", + .format = .r8g8b8a8_unorm, + .extent = .{ + .width = 1, + .height = 1, + .depth = 1, + }, + .mip_levels = 1, + .array_layers = 1, + .samples = .{ .@"1_bit" = true }, + .tiling = .optimal, + .usage = .{ + .transfer_dst_bit = true, + .sampled_bit = true, + }, + .queue_family_indices = &.{ + engine.graphics_queue.allocation.family, + engine.transfer_queue.allocation.family, + }, + .initial_layout = .undefined, + }, + .{ .color_bit = true }, + max_memory, + .{ .device_local_bit = true }, + ); + errdefer engine.freeMemory(device_allocation.device_memory); + // VOLATILE Synchronize with explicit values on top of `Id` type. const empty_base_color_texture = try Texture.init(.{ @@ -89,6 +122,7 @@ pub fn init() !Textures { .height = 1, .usage = .base_color, .target_queue = .graphics, + .device_allocation = &device_allocation, .name = "@Empty", }); array.appendAssumeCapacity(empty_base_color_texture); @@ -98,6 +132,7 @@ pub fn init() !Textures { .height = 1, .usage = .emissive, .target_queue = .graphics, + .device_allocation = &device_allocation, .name = "@Empty", }); array.appendAssumeCapacity(empty_emissive_texture); @@ -107,6 +142,7 @@ pub fn init() !Textures { .height = 1, .usage = .normal, .target_queue = .graphics, + .device_allocation = &device_allocation, .name = "@Empty", }); array.appendAssumeCapacity(empty_normal_texture); @@ -116,6 +152,7 @@ pub fn init() !Textures { .height = 1, .usage = .occlusion_roughness_metallic, .target_queue = .graphics, + .device_allocation = &device_allocation, .name = "@Empty", }); array.appendAssumeCapacity(empty_occlusuion_roughness_metallic_texture); @@ -128,6 +165,7 @@ pub fn init() !Textures { return .{ .map = map, .array = array, + .device_allocation = device_allocation, }; } @@ -139,6 +177,7 @@ pub fn deinit(self: *Textures) void { for (self.array.items) |*texture| { texture.deinit(); } + self.device_allocation.deinit(); self.array.deinit(allocator_general); self.map.deinit(allocator_general); self.* = undefined; @@ -201,7 +240,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); + const texture = try loadTexture(stbi, filename, usage, &self.device_allocation); self.map.putAssumeCapacityNoClobber(key, id); self.array.appendAssumeCapacity(texture); @@ -239,7 +278,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); + const texture = try loadTexture(stbi, filename.toString(), usage, &self.device_allocation); self.map.putAssumeCapacityNoClobber(key, id); self.array.appendAssumeCapacity(texture); @@ -255,6 +294,7 @@ fn loadTexture( stbi: *media.stbi, filename: []const u8, usage: Texture.Usage, + device_allocation: *DeviceAllocation, ) !Texture { const io = ctx.io; @@ -292,6 +332,7 @@ fn loadTexture( .height = img.height, .usage = usage, .target_queue = .graphics, + .device_allocation = device_allocation, .name = filename, }); errdefer texture.deinit(); diff --git a/src/shaders.zig b/src/shaders.zig index c0fb894..b4ea3cd 100644 --- a/src/shaders.zig +++ b/src/shaders.zig @@ -1,9 +1,9 @@ const std = @import("std"); const vm = @import("vecmath"); -const Materials = @import("assets/Materials.zig"); +const Materials = @import("engine/Materials.zig"); const GenericBuffer = @import("engine/GenericBuffer.zig").GenericBuffer; -const Textures = @import("assets/Textures.zig"); +const Textures = @import("engine/Textures.zig"); pub const VertexBuffer = GenericBuffer(void, Vertex); pub const IndexBuffer = GenericBuffer(void, Index); diff --git a/src/voxels.zig b/src/voxels.zig index e24c9f9..9534f2c 100644 --- a/src/voxels.zig +++ b/src/voxels.zig @@ -3,7 +3,7 @@ const std = @import("std"); const c = @import("const.zig"); const vm = @import("vecmath"); -const Materials = @import("assets/Materials.zig"); +const Materials = @import("engine/Materials.zig"); pub const Orientation = enum(u3) { negative_x = 0b111,