From 2992db0901baef05f6613a2a49bab4f87fdb3957 Mon Sep 17 00:00:00 2001 From: johnnyboi12 <54293281+JohnSmoit@users.noreply.github.com> Date: Tue, 22 Jul 2025 21:42:08 -0700 Subject: [PATCH 01/22] refactor(api): update api to better suit compute applications --- src/api/base.zig | 89 +++++++++-------------------------- src/api/graphics_pipeline.zig | 16 +++---- src/api/image.zig | 2 +- src/api/shader.zig | 74 ++++++++++++++++------------- src/api/swapchain.zig | 10 ++-- 5 files changed, 80 insertions(+), 111 deletions(-) diff --git a/src/api/base.zig b/src/api/base.zig index 7d60f1e..be2a051 100644 --- a/src/api/base.zig +++ b/src/api/base.zig @@ -141,12 +141,6 @@ pub const InstanceHandler = struct { } } - // TODO: Conditionally enable validation layers/logging based on config property - // if (config.enable_debug_log) { - // } - - // TODO: make sure our wanted extensions are available - //TODO: Not sure I setup the validatiopn layer configs correctly (uh oh) const instance = try self.w_db.createInstance(&.{ .p_application_info = &.{ .p_application_name = "RayEater_Renderer", @@ -411,6 +405,20 @@ pub const DeviceHandler = struct { return found_indices; } + const DeviceCandidate = struct { + dev: vk.PhysicalDevice, + val: u32, + }; + + fn compareCandidates(ctx: void, a: DeviceCandidate, b: DeviceCandidate) std.math.Order { + _ = ctx; + return if (a.val < b.val) .lt else if (a.val > b.val) .gt else .eq; + } + + + //HACK: Since most modern GPUS are all but guarunteed to support what I'm doing, + //I'm just going to pick the first discrete GPU and call it a day... + // This will likely require a bit of upgrade when I consider making this project more portable fn pickSuitablePhysicalDevice( pr_inst: *const vk.InstanceProxy, config: *const Config, @@ -426,70 +434,17 @@ pub const DeviceHandler = struct { }; defer allocator.free(physical_devices); - var chosen_dev: ?vk.PhysicalDevice = null; - dev_loop: for (physical_devices) |dev| { - const dev_properties = pr_inst.getPhysicalDeviceProperties(dev); - log.debug( - \\Found Device Named {s} - \\ ID: {d} - \\ Type: {d} - \\ - , .{ dev_properties.device_name, dev_properties.device_id, dev_properties.device_type }); - - const dev_queue_indices = getQueueFamilies(dev, pr_inst, config.surface, allocator); - - // check to see if device supports presentation (it must or it crashes) - const supported_extensions = pr_inst.enumerateDeviceExtensionPropertiesAlloc( - dev, - null, - allocator, - ) catch util.emptySlice(vk.ExtensionProperties); - - ext_loop: for (config.required_extensions) |req| { - var found = false; - - for (supported_extensions) |ext| { - if (std.mem.orderZ(u8, @ptrCast(&ext.extension_name), req) == .eq) { - found = true; - break :ext_loop; - } - } - - if (!found) { - continue :dev_loop; - } - } - - //NOTE: Ew - if (dev_queue_indices.graphics_family == null or dev_queue_indices.present_family == null) { - continue; - } - - var dev_present_features = getDeviceSupport( - pr_inst, - config.surface, - dev, - allocator, - ) catch |err| { - log.debug("Failed to query device presentation features due to error: {!}\n", .{err}); - continue; - }; - defer dev_present_features.deinit(); - - if (dev_present_features.formats.len != 0 and dev_present_features.present_modes.len != 0) { - chosen_dev = dev; - log.debug( - \\Chose Device Named {s} - \\ ID: {d} - \\ Type: {d} - \\ - , .{ dev_properties.device_name, dev_properties.device_id, dev_properties.device_type }); - - break; + for (physical_devices) |dev| { + const props = pr_inst.getPhysicalDeviceProperties(dev); + if (props.device_type == .discrete_gpu) { + log.debug("Found suitable device named: {s}", .{props.device_name}); + return dev; } } - return chosen_dev; + _ = config; + + return null; } // Later on, I plan to accept a device properties struct diff --git a/src/api/graphics_pipeline.zig b/src/api/graphics_pipeline.zig index a36ee05..990f804 100644 --- a/src/api/graphics_pipeline.zig +++ b/src/api/graphics_pipeline.zig @@ -24,7 +24,7 @@ pub const FixedFunctionState = struct { /// /// Deez nuts must be enabled pub const Config = struct { - dynamic_states: ?[]const vk.DynamicState, + dynamic_states: []const vk.DynamicState = &.{}, viewport: union(enum) { Swapchain: *const Swapchain, // create viewport from swapchain Direct: struct { // specify fixed function viewport directly @@ -32,8 +32,8 @@ pub const FixedFunctionState = struct { scissor: vk.Rect2D, }, }, - vertex_binding: vk.VertexInputBindingDescription, - vertex_attribs: []const vk.VertexInputAttributeDescription, + vertex_binding: ?vk.VertexInputBindingDescription = null, + vertex_attribs: []const vk.VertexInputAttributeDescription = &.{}, descriptors: []const vk.DescriptorSetLayout, deez_nuts: bool = true, }; @@ -63,7 +63,7 @@ pub const FixedFunctionState = struct { self.pr_dev = ctx.env(.di); - const dynamic_states = config.dynamic_states orelse util.emptySlice(vk.DynamicState); + const dynamic_states = config.dynamic_states; self.dynamic_states = vk.PipelineDynamicStateCreateInfo{ .dynamic_state_count = @intCast(dynamic_states.len), @@ -72,10 +72,10 @@ pub const FixedFunctionState = struct { self.vertex_input = vk.PipelineVertexInputStateCreateInfo{ .vertex_binding_description_count = 1, - .p_vertex_binding_descriptions = util.asManyPtr( + .p_vertex_binding_descriptions = if (config.vertex_binding) |vb| util.asManyPtr( vk.VertexInputBindingDescription, - &config.vertex_binding, - ), + &vb, + ) else null, .vertex_attribute_description_count = @intCast(config.vertex_attribs.len), .p_vertex_attribute_descriptions = config.vertex_attribs.ptr, }; @@ -232,7 +232,7 @@ pr_dev: *const vk.DeviceProxy, viewport_info: vk.Viewport, scissor_info: vk.Rect2D, -pub fn init(ctx: *const Context, config: *const PipelineConfig, allocator: Allocator) !Self { +pub fn init(ctx: *const Context, config: PipelineConfig, allocator: Allocator) !Self { const dev: *const DeviceHandler = ctx.env(.dev); const pipeline_layout = dev.pr_dev.createPipelineLayout( &config.fixed_functions.pipeline_layout_info, diff --git a/src/api/image.zig b/src/api/image.zig index d6a261e..2a8483a 100644 --- a/src/api/image.zig +++ b/src/api/image.zig @@ -51,7 +51,7 @@ pub const Config = struct { width: u32, height: u32, staging_buf: ?*StagingBuffer = null, - initial_layout: vk.ImageLayout, + initial_layout: vk.ImageLayout = .undefined, }; // NOTE: Yet another instance of a BAD function that allocates device memory in a non-zig like fashion diff --git a/src/api/shader.zig b/src/api/shader.zig index 0398ac2..2c1260b 100644 --- a/src/api/shader.zig +++ b/src/api/shader.zig @@ -3,7 +3,6 @@ const rshc = @import("rshc"); const vk = @import("vulkan"); -pub const Stage = rshc.Stage; pub const Context = @import("../context.zig"); const Allocator = std.mem.Allocator; @@ -11,38 +10,55 @@ const log = std.log.scoped(.shader); const DeviceHandler = @import("base.zig").DeviceHandler; -pub fn toShaderStageFlags(stage: Stage) vk.ShaderStageFlags { - return switch (stage) { - .Fragment => vk.ShaderStageFlags{ .fragment_bit = true }, - .Vertex => vk.ShaderStageFlags{ .vertex_bit = true }, - .Compute => vk.ShaderStageFlags{ .compute_bit = true }, - }; -} - pub const Module = struct { - pub const Config = struct { - stage: Stage, - filename: []const u8, - }; + pub const Stage = rshc.Stage; + + fn toShaderStageFlags(stage: Stage) vk.ShaderStageFlags { + return switch (stage) { + .Fragment => vk.ShaderStageFlags{ .fragment_bit = true }, + .Vertex => vk.ShaderStageFlags{ .vertex_bit = true }, + .Compute => vk.ShaderStageFlags{ .compute_bit = true }, + }; + } module: vk.ShaderModule = .null_handle, pipeline_info: vk.PipelineShaderStageCreateInfo = undefined, pr_dev: *const vk.DeviceProxy = undefined, + pub fn initFromBytes(ctx: *const Context, bytes: []const u8, stage: Stage) !Module { + const dev: *const DeviceHandler = ctx.env(.dev); + const module = try dev.pr_dev.createShaderModule(&.{ + .code_size = bytes.len, + .p_code = @as([*]const u32, @alignCast(@ptrCast(bytes.ptr))), + }, null); + + const pipeline_info = vk.PipelineShaderStageCreateInfo{ + .stage = toShaderStageFlags(stage), + .module = module, + .p_name = "main", + }; + + return Module{ + .module = module, + .pipeline_info = pipeline_info, + .pr_dev = &dev.pr_dev, + }; + } + pub fn fromSourceFile( ctx: *const Context, alloc: Allocator, - config: Config, + filename: []const u8, + stage: Stage, ) !Module { - const dev: *const DeviceHandler = ctx.env(.dev); var arena = std.heap.ArenaAllocator.init(alloc); defer arena.deinit(); const allocator = arena.allocator(); const compilation_result = rshc.compileShaderAlloc( - config.filename, - config.stage, + filename, + stage, allocator, ); @@ -53,7 +69,7 @@ pub const Module = struct { val.status, val.message orelse "Unknown", }); - break :fail &[0]u8{}; + break :fail &.{}; } }; @@ -61,22 +77,16 @@ pub const Module = struct { return error.ShaderCompilationError; } - const module = try dev.pr_dev.createShaderModule(&.{ - .code_size = compiled_bytes.len, - .p_code = @alignCast(@ptrCast(compiled_bytes.ptr)), - }, null); + if (@rem(compiled_bytes.len, @sizeOf(u32)) != 0) { + log.warn("Warning: SPIR-V bytes for {s} do not meet alignment requirements (off by {d})", .{ + filename, + @rem(compiled_bytes.len, @sizeOf(u32)), + }); + } - const pipeline_info = vk.PipelineShaderStageCreateInfo{ - .stage = toShaderStageFlags(config.stage), - .module = module, - .p_name = "main", - }; + log.debug("succesfully compiled shader", .{}); - return Module{ - .module = module, - .pipeline_info = pipeline_info, - .pr_dev = &dev.pr_dev, - }; + return initFromBytes(ctx, compiled_bytes, stage); } pub fn deinit(self: *const Module) void { diff --git a/src/api/swapchain.zig b/src/api/swapchain.zig index af50b30..6ee0fdc 100644 --- a/src/api/swapchain.zig +++ b/src/api/swapchain.zig @@ -34,6 +34,7 @@ h_swapchain: vk.SwapchainKHR = undefined, pr_dev: *const vk.DeviceProxy = undefined, images: []ImageInfo = util.emptySlice(ImageInfo), allocator: Allocator, +image_index: u32 = 0, fn chooseSurfaceFormat( available: []const vk.SurfaceFormatKHR, @@ -100,8 +101,10 @@ fn choosePresentMode( } } - log.debug("Chosen fallback present mode: {s}", .{@tagName(.immediate_khr)}); - return chosen_mode orelse .immediate_khr; + return chosen_mode orelse fb: { + log.debug("Chosen fallback present mode: {s}", .{@tagName(.immediate_khr)}); + break :fb .immediate_khr; + }; } fn createImageViews(self: *Self) !void { @@ -247,7 +250,7 @@ pub fn deinit(self: *const Self) void { self.pr_dev.destroySwapchainKHR(self.h_swapchain, null); } -pub fn getNextImage(self: *const Self, sem_signal: ?vk.Semaphore, fence_signal: ?vk.Fence) !u32 { +pub fn getNextImage(self: *Self, sem_signal: ?vk.Semaphore, fence_signal: ?vk.Fence) !u32 { const res = try self.pr_dev.acquireNextImageKHR( self.h_swapchain, std.math.maxInt(u64), @@ -255,5 +258,6 @@ pub fn getNextImage(self: *const Self, sem_signal: ?vk.Semaphore, fence_signal: fence_signal orelse .null_handle, ); + self.image_index = res.image_index; return res.image_index; } From 060ca1ae05e3382fd66b4283fe9abdcb765819fe Mon Sep 17 00:00:00 2001 From: johnnyboi12 <54293281+JohnSmoit@users.noreply.github.com> Date: Tue, 22 Jul 2025 21:43:11 -0700 Subject: [PATCH 02/22] chore(helpers): make helpers its own module to be imported by each sample --- build.zig | 13 ++++++++++++- build.zig.zon | 4 ++-- samples/common/helpers.zig | 18 ++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/build.zig b/build.zig index 52757f0..8a279a2 100644 --- a/build.zig +++ b/build.zig @@ -93,7 +93,7 @@ const SampleEntry = struct { }; var sample_files = [_]SampleEntry{ .{ .name = "basic_planes", .path = "basic_planes.zig" }, - .{ .name = "basic_compute", .path = "basic_compute.zig" }, + .{ .name = "slime", .path = "slime/main.zig" }, .{ .name = "test_sample", .path = "test_sample.zig" }, }; @@ -103,6 +103,16 @@ fn populateSampleModules( deps: Dependencies, opts: BuildOpts, ) void { + const sample_commons = b.createModule(.{ + .root_source_file = b.path("samples/common/helpers.zig"), + .optimize = opts.optimize, + .target = opts.target, + }); + + sample_commons.addImport("ray", lib_mod); + sample_commons.addImport("glfw", deps.glfw); + sample_commons.addImport("vulkan", deps.vulkan); + for (&sample_files) |*f| { const sample_mod = b.createModule(.{ .root_source_file = b.path(b.pathJoin(&.{ @@ -116,6 +126,7 @@ fn populateSampleModules( sample_mod.addImport("ray", lib_mod); sample_mod.addImport("glfw", deps.glfw); sample_mod.addImport("vulkan", deps.vulkan); + sample_mod.addImport("helpers", sample_commons); const sample_exe = b.addExecutable(.{ .name = f.name, diff --git a/build.zig.zon b/build.zig.zon index 1b4a25b..482c826 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -49,8 +49,8 @@ .hash = "N-V-__8AAOlnwQD_q2VUB2dZD64hwX7kJ2w9knrChBKgJHe0", }, .RshLang = .{ - .url = "git+https://github.com/JohnSmoit/ray-eater-shaders.git#a5c9c1240b9bbcdaab1536d909c22facf548ab7a", - .hash = "RshLang-0.0.0-4EjkCyFmAADvzF5CNrhKYg2-gA09u8Q10ZlJAHbbCwoI", + .url = "git+https://github.com/JohnSmoit/ray-eater-shaders.git#5fe9defc2e4279272af5fb442b093d5a89a23609", + .hash = "RshLang-0.0.0-4EjkC4hnAADKED6rCb7dvYU5MJutrKPz8eIWuaEWXyfO", }, }, .paths = .{ diff --git a/samples/common/helpers.zig b/samples/common/helpers.zig index 39db6ed..84a5316 100644 --- a/samples/common/helpers.zig +++ b/samples/common/helpers.zig @@ -5,8 +5,18 @@ const vk = @import("vulkan"); const glfw = @import("glfw"); +const ray = @import("ray"); + +const ShaderModule = ray.api.ShaderModule; +const Stage = ShaderModule.Stage; +const Context = ray.Context; + +const Allocator = std.mem.Allocator; + const Window = glfw.Window; +pub const RenderQuad = @import("render_quad.zig"); + pub fn makeBasicWindow(w: u32, h: u32, name: []const u8) !Window { glfw.init() catch |err| { std.debug.print("Failed to initialize GLFW\n", .{}); @@ -38,3 +48,11 @@ pub fn windowExtent(win: *const Window) vk.Extent2D { .height = dims.height, }; } + +pub fn initSampleShader(ctx: *const Context, allocator: Allocator, path: []const u8, stage: Stage) !ShaderModule { + const base: []const u8 = "samples/"; + const final_path = try std.mem.concat(allocator, u8, &.{base, path}); + defer allocator.free(final_path); + + return ShaderModule.fromSourceFile(ctx, allocator, final_path, stage); +} From 21b817e711564b917b4cefebd2c6ea5cb47cb6d5 Mon Sep 17 00:00:00 2001 From: johnnyboi12 <54293281+JohnSmoit@users.noreply.github.com> Date: Tue, 22 Jul 2025 21:43:40 -0700 Subject: [PATCH 03/22] feat(compute): begin implementing a compute-shader demonstration via a slime-mold simulation --- samples/basic_compute.zig | 132 ------------------------ samples/basic_planes.zig | 2 +- samples/common/render_quad.zig | 118 +++++++++++++++++++++ samples/slime/frag.glsl | 8 ++ samples/slime/frag.glsl.spv | Bin 0 -> 568 bytes samples/slime/main.zig | 182 +++++++++++++++++++++++++++++++++ 6 files changed, 309 insertions(+), 133 deletions(-) delete mode 100644 samples/basic_compute.zig create mode 100644 samples/common/render_quad.zig create mode 100644 samples/slime/frag.glsl create mode 100644 samples/slime/frag.glsl.spv create mode 100644 samples/slime/main.zig diff --git a/samples/basic_compute.zig b/samples/basic_compute.zig deleted file mode 100644 index 10a65ac..0000000 --- a/samples/basic_compute.zig +++ /dev/null @@ -1,132 +0,0 @@ -const std = @import("std"); -const ray = @import("ray"); -const glfw = @import("glfw"); -const helpers = @import("common/helpers.zig"); - -const api = ray.api; - -const FixedBufferAllocator = std.heap.FixedBufferAllocator; -const Allocator = std.mem.Allocator; - -const Context = ray.Context; -const Swapchain = api.Swapchain; -const FrameBuffer = api.FrameBuffer; -const GraphicsPipeline = api.GraphicsPipeline; -const RenderPass = api.RenderPass; -const FixedFunctionState = api.FixedFunctionState; -const Descriptor = api.Descriptor; -const DescriptorBinding = api.ResolvedDescriptorBinding; - -const log = std.log.scoped(.application); - -const GraphicsState = struct { - descriptor: Descriptor, - renderpass: RenderPass, - framebuffers: FrameBuffer, - pipeline: GraphicsPipeline, -}; - -const SampleState = struct { - ctx: *Context = undefined, - swapchain: Swapchain = undefined, - window: glfw.Window = undefined, - allocator: Allocator, - - graphics: GraphicsState = undefined, - - pub fn createContext(self: *SampleState) !void { - self.window = try helpers.makeBasicWindow(900, 600, "BAD APPLE >:}"); - self.ctx = try Context.init(self.allocator, .{ - .inst_extensions = helpers.glfwInstanceExtensions(), - .loader = glfw.glfwGetInstanceProcAddress, - .window = &self.window, - }); - } - - pub fn createSwapchain(self: *SampleState) !void { - self.swapchain = try Swapchain.init(self.ctx, self.allocator, .{ - .requested_extent = helpers.windowExtent(&self.window), - .requested_present_mode = .mailbox_khr, - .requested_format = .{ - .color_space = .srgb_nonlinear_khr, - .format = .r8g8b8a8_srgb, - }, - }); - } - - pub fn createGraphicsPipelines(self: *SampleState) !void { - const desc_bindings = [_]DescriptorBinding{.{}}; - - self.graphics.descriptor = try Descriptor.init(self.ctx, self.allocator, .{ - .bindings = desc_bindings[0..], - }); - const fixed_functions = FixedFunctionState{}; - //TODO: Rename this function - try fixed_functions.init_self(self.ctx, .{ - }); - defer fixed_functions.deinit(); - - const attachments = [_]RenderPass.ConfigEntry{.{ - .attachment = .{ - .format = .r8g8b8a8_srgb, - .initial_layout = .undefined, - .final_layout = .color_attachment_optimal, - .load_op = .clear, - .store_op = .store, - .stencil_load_op = .dont_care, - .stencil_store_op = .dont_care, - }, - .tipo = .Color, - }}; - self.graphics.renderpass = try RenderPass.initAlloc(self.ctx, self.allocator, attachments[0..]); - - self.graphics.framebuffers = try FrameBuffer.initAlloc(self.ctx, self.allocator, .{ - .image_views = self.swapchain.images, - .extent = helpers.windowExtent(&self.window), - .renderpass = &self.graphics.renderpass, - }); - - self.graphics.pipeline = try GraphicsPipeline.init(self.ctx, .{ - .renderpass = &self.graphics.renderpass, - .fixed_functions = &fixed_functions, - .shader_stages = undefined, - }, self.allocator); - } - - pub fn active(self: *const SampleState) bool { - return !self.window.shouldClose(); - } - - // intercepts errors and logs them - pub fn update(self: *SampleState) void { - glfw.pollEvents(); - _ = self; - } - - pub fn deinit(self: *SampleState) void { - self.swapchain.deinit(); - self.ctx.deinit(); - self.window.destroy(); - } -}; - -pub fn main() !void { - const mem = try std.heap.page_allocator.alloc(u8, 1_000_024); - defer std.heap.page_allocator.free(mem); - - var buf_alloc = FixedBufferAllocator.init(mem); - var state: SampleState = .{ - .allocator = buf_alloc.allocator(), - }; - - try state.createContext(); - try state.createSwapchain(); - state.window.show(); - - while (state.active()) { - state.update(); - } - - state.deinit(); - log.info("You Win!", .{}); -} diff --git a/samples/basic_planes.zig b/samples/basic_planes.zig index 018ed09..d66c596 100644 --- a/samples/basic_planes.zig +++ b/samples/basic_planes.zig @@ -8,7 +8,7 @@ const ray = @import("ray"); const math = ray.math; const api = ray.api; -const helpers = @import("common/helpers.zig"); +const helpers = @import("helpers"); const util = ray.util; const span = util.span; diff --git a/samples/common/render_quad.zig b/samples/common/render_quad.zig new file mode 100644 index 0000000..aaa81d2 --- /dev/null +++ b/samples/common/render_quad.zig @@ -0,0 +1,118 @@ +const std = @import("std"); +const ray = @import("ray"); +const api = ray.api; + +const Allocator = std.mem.Allocator; + +const Self = @This(); + +const Context = ray.Context; + +const Descriptor = api.Descriptor; +const GraphicsPipeline = api.GraphicsPipeline; +const FixedFunctionState = api.FixedFunctionState; +const ShaderModule = api.ShaderModule; +const RenderPass = api.RenderPass; +const CommandBuffer = api.CommandBuffer; +const DeviceHandler = api.DeviceHandler; +const FrameBuffer = api.FrameBuffer; + +const Swapchain = api.Swapchain; + +desc: Descriptor = undefined, +pipeline: GraphicsPipeline = undefined, +renderpass: RenderPass = undefined, +dev: *const DeviceHandler = undefined, +swapchain: *const Swapchain = undefined, + +const hardcoded_vert_src: []const u8 = + \\ #version 450 + \\ vec2 verts[4] = vec2[]( + \\ vec2(-1.0, -1.0), + \\ vec2( 1.0, -1.0), + \\ vec2( 1.0, 1.0), + \\ vec2(-1.0, 1.0) + \\ ); + \\ vec2 uvs[4] = vec2[]( + \\ vec2(0.0, 0.0), + \\ vec2(1.0, 0.0), + \\ vec2(1.0, 1.0), + \\ vec2(0.0, 1.0) + \\); + \\ layout(location = 0) out vec2 texCoord; + \\ + \\ void main() { + \\ gl_Position = vec4(verts[gl_VertexIndex], 0.0, 1.0); + \\ texCoord = uvs[gl_VertexIndex]; + \\ } +; + +pub const Config = struct { + // null if fragment shader has no referenced data + // fragment descriptors are also combined with vertex descriptors + frag_descriptors: ?*const Descriptor = null, + frag_shader: *const ShaderModule, + swapchain: *const Swapchain, +}; + +pub fn initSelf(self: *Self, ctx: *const Context, allocator: Allocator, config: Config) !void { + const vert_shader = try ShaderModule.initFromBytes(ctx, hardcoded_vert_src, .Vertex); + defer vert_shader.deinit(); + + const shaders: []const ShaderModule = &.{ + vert_shader, + config.frag_shader.*, + }; + + var fixed_functions_config = FixedFunctionState{}; + fixed_functions_config.init_self(ctx, &.{ + .dynamic_states = &.{ + .viewport, + .scissor, + }, + .viewport = .{ .Swapchain = config.swapchain }, + .descriptors = if (config.frag_descriptors) |fd| &.{ + fd.h_desc_layout, + } else &.{}, + }); + defer fixed_functions_config.deinit(); + + self.renderpass = try api.RenderPass.initAlloc(ctx, allocator, &.{.{ + .attachment = .{ + .initial_layout = .undefined, + .final_layout = .present_src_khr, + .format = .r8g8b8a8_srgb, + + .load_op = .clear, + .store_op = .store, + + .stencil_load_op = .dont_care, + .stencil_store_op = .dont_care, + .samples = .{ .@"1_bit" = true }, + }, + .tipo = .Color, + }}); + + self.pipeline = try GraphicsPipeline.init(ctx, .{ + .fixed_functions = &fixed_functions_config, + .renderpass = &self.renderpass, + .shader_stages = shaders, + }, allocator); + + self.dev = ctx.env(.dev); + self.swapchain = config.swapchain; +} + +pub fn drawOneShot(self: *const Self, cmd_buf: *const CommandBuffer, framebuffer: *const FrameBuffer) void { + self.pipeline.bind(cmd_buf); + const image_index = self.swapchain.image_index; + self.renderpass.begin(cmd_buf, framebuffer, image_index); + self.dev.draw(cmd_buf, 4, 0, 0, 0); + self.renderpass.end(cmd_buf); +} + +pub fn deinit(self: *Self) void { + self.pipeline.deinit(); + self.renderpass.deinit(); + self.desc.deinit(); +} diff --git a/samples/slime/frag.glsl b/samples/slime/frag.glsl new file mode 100644 index 0000000..74b7630 --- /dev/null +++ b/samples/slime/frag.glsl @@ -0,0 +1,8 @@ +#version 450 + +layout(location = 0) out vec4 fragColor; +layout(location = 0) in vec2 texCoord; + +void main() { + fragColor = vec4(texCoord, 0.0, 1.0); +} diff --git a/samples/slime/frag.glsl.spv b/samples/slime/frag.glsl.spv new file mode 100644 index 0000000000000000000000000000000000000000..bc8bbf92c9396b0428dbda266f748a27ef40cd5b GIT binary patch literal 568 zcmYk2PfNo<6vUrR)7IAhv!FMrcod2UErO_rBDn;r_yIy`q6A`7(kOcKv-zpK2+nV9 zr3)`_XLk0@Y`n@z-E7BN*0zEDt-e*nn7EqjLHHPsR%JXozc@qDG0zFnbggbxb?Rl7 zE;!g#?Wuaw!OK89PgBjlbg61h3&JRvPJ{3+nlG2ps{F{3C`%Vf6sJWpFVnXKU1Lj^ zF6P;5ye;sh*!K0!civCM?0J%Bc_BYIvXsgDB+rXjTMKlHj7I=1Fq#b>C@EQwU21dt z1MO?#hN?s9c@r4_p}q}u{tEon8sha97of3Cmg@Vr-YvhSB Ct1q+w literal 0 HcmV?d00001 diff --git a/samples/slime/main.zig b/samples/slime/main.zig new file mode 100644 index 0000000..84c4026 --- /dev/null +++ b/samples/slime/main.zig @@ -0,0 +1,182 @@ +const std = @import("std"); +const ray = @import("ray"); +const glfw = @import("glfw"); +const helpers = @import("helpers"); +const vk = @import("vulkan"); + +const api = ray.api; +const math = ray.math; + +const Vec3 = math.Vec3; + +const RenderQuad = helpers.RenderQuad; + +const FixedBufferAllocator = std.heap.FixedBufferAllocator; +const Allocator = std.mem.Allocator; + +const Context = ray.Context; +const Swapchain = api.Swapchain; +const FrameBuffer = api.FrameBuffer; +const GraphicsPipeline = api.GraphicsPipeline; +const RenderPass = api.RenderPass; +const FixedFunctionState = api.FixedFunctionState; +const Descriptor = api.Descriptor; +const DescriptorBinding = api.ResolvedDescriptorBinding; +const CommandBuffer = api.CommandBuffer; + +const GraphicsQueue = api.GraphicsQueue; +const PresentQueue = api.PresentQueue; + +const UniformBuffer = api.ComptimeUniformBuffer; +const TexImage = api.Image; +const Compute = api.Compute; + +const log = std.log.scoped(.application); + +const GraphicsState = struct { + framebuffers: FrameBuffer, + render_quad: RenderQuad, + + cmd_buf: CommandBuffer, + + // pipeline for running slime simulation + slime_pipeline: Compute, + + // pipeline for updating pheremone map + stinky_pipeline: Compute, + + pub fn deinit(self: *GraphicsState) void { + self.render_quad.deinit(); + self.framebuffers.deinit(); + self.cmd_buf.deinit(); + } +}; + +const ApplicationUniforms = extern struct { + time: f32, +}; + +const GPUState = struct { + host_uniforms: ApplicationUniforms, + + // compute and graphics visible + uniforms: UniformBuffer(ApplicationUniforms), + + // compute and graphics visible (needs viewport quad) + // written to in the compute shader and simply mapped to the viewport + // with a few transformations for color and such. + // ... This also represents the pheremone map that sim agents in the compute + // shader would use to determine their movements + // + render_target: TexImage, + + // compute visible only + // (contains source image data to base simulation off of) + src_image: TexImage, + + // compute visible only + // -- contains simulation agents + //particles: StorageBuffer, +}; + +const SampleState = struct { + ctx: *Context = undefined, + swapchain: Swapchain = undefined, + window: glfw.Window = undefined, + allocator: Allocator, + + graphics: GraphicsState = undefined, + gpu_state: GPUState = undefined, + + present_queue: PresentQueue = undefined, + graphics_queue: GraphicsQueue = undefined, + + pub fn createContext(self: *SampleState) !void { + self.window = try helpers.makeBasicWindow(900, 600, "BAD APPLE >:}"); + self.ctx = try Context.init(self.allocator, .{ + .inst_extensions = helpers.glfwInstanceExtensions(), + .loader = glfw.glfwGetInstanceProcAddress, + .window = &self.window, + }); + } + + pub fn createSwapchain(self: *SampleState) !void { + self.swapchain = try Swapchain.init(self.ctx, self.allocator, .{ + .requested_extent = helpers.windowExtent(&self.window), + .requested_present_mode = .mailbox_khr, + .requested_format = .{ + .color_space = .srgb_nonlinear_khr, + .format = .r8g8b8a8_srgb, + }, + }); + } + + pub fn retrieveDeviceQueues(self: *SampleState) !void { + self.graphics_queue = try api.GraphicsQueue.init(self.ctx); + self.present_queue = try api.PresentQueue.init(self.ctx); + } + + pub fn createGraphicsPipeline(self: *SampleState) !void { + const frag_shader = try helpers.initSampleShader( + self.ctx, + self.allocator, + "slime/frag.glsl", + .Fragment, + ); + + defer frag_shader.deinit(); + + try self.graphics.render_quad.initSelf(self.ctx, self.allocator, .{ + .frag_shader = &frag_shader, + .swapchain = &self.swapchain, + }); + + self.graphics.cmd_buf = try CommandBuffer.init(self.ctx); + } + + pub fn active(self: *const SampleState) bool { + return !self.window.shouldClose(); + } + + // intercepts errors and logs them + pub fn update(self: *SampleState) !void { + glfw.pollEvents(); + try self.graphics.cmd_buf.begin(); + self.graphics.render_quad.drawOneShot( + &self.graphics.cmd_buf, + &self.graphics.framebuffers, + ); + + try self.graphics.cmd_buf.end(); + } + + pub fn deinit(self: *SampleState) void { + self.graphics.deinit(); + + self.swapchain.deinit(); + self.ctx.deinit(); + self.window.destroy(); + } +}; + +pub fn main() !void { + const mem = try std.heap.page_allocator.alloc(u8, 1_000_024); + defer std.heap.page_allocator.free(mem); + + var buf_alloc = FixedBufferAllocator.init(mem); + var state: SampleState = .{ + .allocator = buf_alloc.allocator(), + }; + + try state.createContext(); + try state.createSwapchain(); + try state.createGraphicsPipeline(); + state.window.show(); + + while (state.active()) { + try state.update(); + } + + state.deinit(); + log.info("You Win!", .{}); +} From dcb1274c1f241e0c77fccb4219d6cbf07cd1c6ce Mon Sep 17 00:00:00 2001 From: johnnyboi12 <54293281+JohnSmoit@users.noreply.github.com> Date: Thu, 24 Jul 2025 21:42:50 -0600 Subject: [PATCH 04/22] fix(graphics_pipeline): fixed issue when specifying no vertex inputs --- src/api/graphics_pipeline.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/graphics_pipeline.zig b/src/api/graphics_pipeline.zig index 990f804..34ff985 100644 --- a/src/api/graphics_pipeline.zig +++ b/src/api/graphics_pipeline.zig @@ -71,7 +71,7 @@ pub const FixedFunctionState = struct { }; self.vertex_input = vk.PipelineVertexInputStateCreateInfo{ - .vertex_binding_description_count = 1, + .vertex_binding_description_count = if (config.vertex_binding != null) 1 else 0, .p_vertex_binding_descriptions = if (config.vertex_binding) |vb| util.asManyPtr( vk.VertexInputBindingDescription, &vb, From 4e9d46539e2727ea8d8ac2820f2f88e4517faf86 Mon Sep 17 00:00:00 2001 From: johnnyboi12 <54293281+JohnSmoit@users.noreply.github.com> Date: Thu, 24 Jul 2025 21:44:46 -0600 Subject: [PATCH 05/22] chore(framebuffer): update framebuffer initialization to be parametarized based on a swapchain --- src/api/frame_buffer.zig | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/api/frame_buffer.zig b/src/api/frame_buffer.zig index ede9281..b277780 100644 --- a/src/api/frame_buffer.zig +++ b/src/api/frame_buffer.zig @@ -15,34 +15,37 @@ pub const FrameBufferInfo = struct { }; pub const Config = struct { renderpass: *const RenderPass, - image_views: []const Swapchain.ImageInfo, + swapchain: *const Swapchain, depth_view: ?vk.ImageView = null, - extent: vk.Rect2D, }; framebuffers: []vk.Framebuffer, pr_dev: *const vk.DeviceProxy, allocator: Allocator, -extent: vk.Rect2D, +extent: vk.Extent2D, /// ## Notes /// Unfortunately, allocation is neccesary due to the runtime count of the swapchain /// images -pub fn initAlloc(ctx: *const Context, allocator: Allocator, config: *const Config) !Self { +pub fn initAlloc(ctx: *const Context, allocator: Allocator, config: Config) !Self { const pr_dev: *const vk.DeviceProxy = ctx.env(.di); - var framebuffers = try allocator.alloc(vk.Framebuffer, config.image_views.len); + + const image_views = config.swapchain.images; + const extent = config.swapchain.extent; + + var framebuffers = try allocator.alloc(vk.Framebuffer, image_views.len); const attachment_count: u32 = if (config.depth_view != null) 2 else 1; - for (config.image_views, 0..) |*info, index| { + for (image_views, 0..) |*info, index| { const views = [2]vk.ImageView{ info.h_view, config.depth_view orelse .null_handle }; framebuffers[index] = try pr_dev.createFramebuffer(&.{ .render_pass = config.renderpass.h_rp, .attachment_count = attachment_count, .p_attachments = views[0..], - .width = config.extent.extent.width, - .height = config.extent.extent.height, + .width = extent.width, + .height = extent.height, .layers = 1, }, null); } @@ -51,14 +54,17 @@ pub fn initAlloc(ctx: *const Context, allocator: Allocator, config: *const Confi .framebuffers = framebuffers, .pr_dev = pr_dev, .allocator = allocator, - .extent = config.extent, + .extent = extent, }; } pub fn get(self: *const Self, image_index: u32) FrameBufferInfo { return FrameBufferInfo{ .h_framebuffer = self.framebuffers[@intCast(image_index)], - .extent = self.extent, + .extent = vk.Rect2D{ + .offset = .{ .x = 0, .y = 0 }, + .extent = self.extent, + }, }; } From a4514e47e26e4a2273eca4c5f121d17f2cf34a20 Mon Sep 17 00:00:00 2001 From: johnnyboi12 <54293281+JohnSmoit@users.noreply.github.com> Date: Thu, 24 Jul 2025 21:45:31 -0600 Subject: [PATCH 06/22] refactor(shader): refactor shader modules to use updated rshc with shaderc --- src/api/shader.zig | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/api/shader.zig b/src/api/shader.zig index 2c1260b..39a2ff5 100644 --- a/src/api/shader.zig +++ b/src/api/shader.zig @@ -9,9 +9,10 @@ const Allocator = std.mem.Allocator; const log = std.log.scoped(.shader); const DeviceHandler = @import("base.zig").DeviceHandler; +const Compiler = rshc.ShaderCompiler; pub const Module = struct { - pub const Stage = rshc.Stage; + pub const Stage = Compiler.Stage; fn toShaderStageFlags(stage: Stage) vk.ShaderStageFlags { return switch (stage) { @@ -25,6 +26,27 @@ pub const Module = struct { pipeline_info: vk.PipelineShaderStageCreateInfo = undefined, pr_dev: *const vk.DeviceProxy = undefined, + pub fn initFromSrc(ctx: *const Context, allocator: Allocator, src: []const u8, stage: Stage) !Module { + var compiler = try Compiler.init(allocator); + defer compiler.deinit(); + + const res = compiler.fromSrc(src, stage); + + return switch(res) { + .Success => |bytes| initFromBytes(ctx, bytes, stage), + .Failure => |v| fb: { + log.err("An error occured while compiling the shader: {!}\nMessage: {s}", .{ + v.status, + v.message orelse "(no message)" + }); + + break :fb v.status; + } + }; + } + + /// Directly initialize from compiled SPIRV binaries + /// (not to be confused with source strings) pub fn initFromBytes(ctx: *const Context, bytes: []const u8, stage: Stage) !Module { const dev: *const DeviceHandler = ctx.env(.dev); const module = try dev.pr_dev.createShaderModule(&.{ @@ -55,11 +77,12 @@ pub const Module = struct { defer arena.deinit(); const allocator = arena.allocator(); + var compiler = try Compiler.init(allocator); + defer compiler.deinit(); - const compilation_result = rshc.compileShaderAlloc( + const compilation_result = compiler.fromFile( filename, stage, - allocator, ); const compiled_bytes: []const u8 = switch (compilation_result) { @@ -83,6 +106,7 @@ pub const Module = struct { @rem(compiled_bytes.len, @sizeOf(u32)), }); } + log.debug("SPIRV bytes size: {d}", .{compiled_bytes.len}); log.debug("succesfully compiled shader", .{}); From 0105adaadf975aaaa0ab6b1ebc53ae5fb305c048 Mon Sep 17 00:00:00 2001 From: johnnyboi12 <54293281+JohnSmoit@users.noreply.github.com> Date: Thu, 24 Jul 2025 21:46:12 -0600 Subject: [PATCH 07/22] Revert "chore(framebuffer): update framebuffer initialization to be parametarized based on a swapchain" This reverts commit 4e9d46539e2727ea8d8ac2820f2f88e4517faf86. --- src/api/frame_buffer.zig | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/api/frame_buffer.zig b/src/api/frame_buffer.zig index b277780..ede9281 100644 --- a/src/api/frame_buffer.zig +++ b/src/api/frame_buffer.zig @@ -15,37 +15,34 @@ pub const FrameBufferInfo = struct { }; pub const Config = struct { renderpass: *const RenderPass, - swapchain: *const Swapchain, + image_views: []const Swapchain.ImageInfo, depth_view: ?vk.ImageView = null, + extent: vk.Rect2D, }; framebuffers: []vk.Framebuffer, pr_dev: *const vk.DeviceProxy, allocator: Allocator, -extent: vk.Extent2D, +extent: vk.Rect2D, /// ## Notes /// Unfortunately, allocation is neccesary due to the runtime count of the swapchain /// images -pub fn initAlloc(ctx: *const Context, allocator: Allocator, config: Config) !Self { +pub fn initAlloc(ctx: *const Context, allocator: Allocator, config: *const Config) !Self { const pr_dev: *const vk.DeviceProxy = ctx.env(.di); - - const image_views = config.swapchain.images; - const extent = config.swapchain.extent; - - var framebuffers = try allocator.alloc(vk.Framebuffer, image_views.len); + var framebuffers = try allocator.alloc(vk.Framebuffer, config.image_views.len); const attachment_count: u32 = if (config.depth_view != null) 2 else 1; - for (image_views, 0..) |*info, index| { + for (config.image_views, 0..) |*info, index| { const views = [2]vk.ImageView{ info.h_view, config.depth_view orelse .null_handle }; framebuffers[index] = try pr_dev.createFramebuffer(&.{ .render_pass = config.renderpass.h_rp, .attachment_count = attachment_count, .p_attachments = views[0..], - .width = extent.width, - .height = extent.height, + .width = config.extent.extent.width, + .height = config.extent.extent.height, .layers = 1, }, null); } @@ -54,17 +51,14 @@ pub fn initAlloc(ctx: *const Context, allocator: Allocator, config: Config) !Sel .framebuffers = framebuffers, .pr_dev = pr_dev, .allocator = allocator, - .extent = extent, + .extent = config.extent, }; } pub fn get(self: *const Self, image_index: u32) FrameBufferInfo { return FrameBufferInfo{ .h_framebuffer = self.framebuffers[@intCast(image_index)], - .extent = vk.Rect2D{ - .offset = .{ .x = 0, .y = 0 }, - .extent = self.extent, - }, + .extent = self.extent, }; } From 02e606e874923f181ef6a33efceea662e4a5087f Mon Sep 17 00:00:00 2001 From: johnnyboi12 <54293281+JohnSmoit@users.noreply.github.com> Date: Thu, 24 Jul 2025 21:47:59 -0600 Subject: [PATCH 08/22] chore(framebuffer): update framebuffer initailization to be parameteraized based on swapchain --- src/api/frame_buffer.zig | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/api/frame_buffer.zig b/src/api/frame_buffer.zig index ede9281..b277780 100644 --- a/src/api/frame_buffer.zig +++ b/src/api/frame_buffer.zig @@ -15,34 +15,37 @@ pub const FrameBufferInfo = struct { }; pub const Config = struct { renderpass: *const RenderPass, - image_views: []const Swapchain.ImageInfo, + swapchain: *const Swapchain, depth_view: ?vk.ImageView = null, - extent: vk.Rect2D, }; framebuffers: []vk.Framebuffer, pr_dev: *const vk.DeviceProxy, allocator: Allocator, -extent: vk.Rect2D, +extent: vk.Extent2D, /// ## Notes /// Unfortunately, allocation is neccesary due to the runtime count of the swapchain /// images -pub fn initAlloc(ctx: *const Context, allocator: Allocator, config: *const Config) !Self { +pub fn initAlloc(ctx: *const Context, allocator: Allocator, config: Config) !Self { const pr_dev: *const vk.DeviceProxy = ctx.env(.di); - var framebuffers = try allocator.alloc(vk.Framebuffer, config.image_views.len); + + const image_views = config.swapchain.images; + const extent = config.swapchain.extent; + + var framebuffers = try allocator.alloc(vk.Framebuffer, image_views.len); const attachment_count: u32 = if (config.depth_view != null) 2 else 1; - for (config.image_views, 0..) |*info, index| { + for (image_views, 0..) |*info, index| { const views = [2]vk.ImageView{ info.h_view, config.depth_view orelse .null_handle }; framebuffers[index] = try pr_dev.createFramebuffer(&.{ .render_pass = config.renderpass.h_rp, .attachment_count = attachment_count, .p_attachments = views[0..], - .width = config.extent.extent.width, - .height = config.extent.extent.height, + .width = extent.width, + .height = extent.height, .layers = 1, }, null); } @@ -51,14 +54,17 @@ pub fn initAlloc(ctx: *const Context, allocator: Allocator, config: *const Confi .framebuffers = framebuffers, .pr_dev = pr_dev, .allocator = allocator, - .extent = config.extent, + .extent = extent, }; } pub fn get(self: *const Self, image_index: u32) FrameBufferInfo { return FrameBufferInfo{ .h_framebuffer = self.framebuffers[@intCast(image_index)], - .extent = self.extent, + .extent = vk.Rect2D{ + .offset = .{ .x = 0, .y = 0 }, + .extent = self.extent, + }, }; } From 04a04f513abfcb16debc210d84a273695239ba8d Mon Sep 17 00:00:00 2001 From: johnnyboi12 <54293281+JohnSmoit@users.noreply.github.com> Date: Thu, 24 Jul 2025 21:48:41 -0600 Subject: [PATCH 09/22] feat(slime): complete render quad's graphics pipeline --- build.zig.zon | 4 ++-- samples/common/render_quad.zig | 9 ++++++--- samples/slime/main.zig | 10 ++++++++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 482c826..d6ff12a 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -49,8 +49,8 @@ .hash = "N-V-__8AAOlnwQD_q2VUB2dZD64hwX7kJ2w9knrChBKgJHe0", }, .RshLang = .{ - .url = "git+https://github.com/JohnSmoit/ray-eater-shaders.git#5fe9defc2e4279272af5fb442b093d5a89a23609", - .hash = "RshLang-0.0.0-4EjkC4hnAADKED6rCb7dvYU5MJutrKPz8eIWuaEWXyfO", + .url = "git+https://github.com/JohnSmoit/ray-eater-shaders.git#621811032c0d385a1c49d5f655e2498a1de72ab9", + .hash = "RshLang-0.0.0-4EjkC1twAACu3mtJTUJzOjluiAhJQzA5rDEGfYHgaMnD", }, }, .paths = .{ diff --git a/samples/common/render_quad.zig b/samples/common/render_quad.zig index aaa81d2..fda5dbb 100644 --- a/samples/common/render_quad.zig +++ b/samples/common/render_quad.zig @@ -19,7 +19,6 @@ const FrameBuffer = api.FrameBuffer; const Swapchain = api.Swapchain; -desc: Descriptor = undefined, pipeline: GraphicsPipeline = undefined, renderpass: RenderPass = undefined, dev: *const DeviceHandler = undefined, @@ -56,7 +55,12 @@ pub const Config = struct { }; pub fn initSelf(self: *Self, ctx: *const Context, allocator: Allocator, config: Config) !void { - const vert_shader = try ShaderModule.initFromBytes(ctx, hardcoded_vert_src, .Vertex); + const vert_shader = try ShaderModule.initFromSrc( + ctx, + allocator, + hardcoded_vert_src, + .Vertex, + ); defer vert_shader.deinit(); const shaders: []const ShaderModule = &.{ @@ -114,5 +118,4 @@ pub fn drawOneShot(self: *const Self, cmd_buf: *const CommandBuffer, framebuffer pub fn deinit(self: *Self) void { self.pipeline.deinit(); self.renderpass.deinit(); - self.desc.deinit(); } diff --git a/samples/slime/main.zig b/samples/slime/main.zig index 84c4026..c76addc 100644 --- a/samples/slime/main.zig +++ b/samples/slime/main.zig @@ -109,6 +109,7 @@ const SampleState = struct { .format = .r8g8b8a8_srgb, }, }); + } pub fn retrieveDeviceQueues(self: *SampleState) !void { @@ -131,6 +132,12 @@ const SampleState = struct { .swapchain = &self.swapchain, }); + self.graphics.framebuffers = try FrameBuffer.initAlloc(self.ctx, self.allocator, .{ + .depth_view = null, + .swapchain = &self.swapchain, + .renderpass = &self.graphics.render_quad.renderpass, + }); + self.graphics.cmd_buf = try CommandBuffer.init(self.ctx); } @@ -141,6 +148,8 @@ const SampleState = struct { // intercepts errors and logs them pub fn update(self: *SampleState) !void { glfw.pollEvents(); + + try self.graphics.cmd_buf.reset(); try self.graphics.cmd_buf.begin(); self.graphics.render_quad.drawOneShot( &self.graphics.cmd_buf, @@ -169,6 +178,7 @@ pub fn main() !void { }; try state.createContext(); + try state.retrieveDeviceQueues(); try state.createSwapchain(); try state.createGraphicsPipeline(); state.window.show(); From 571b21d10fca366728ff1e9ebac75ea64602c8fb Mon Sep 17 00:00:00 2001 From: johnnyboi12 <54293281+JohnSmoit@users.noreply.github.com> Date: Fri, 25 Jul 2025 19:15:36 -0600 Subject: [PATCH 10/22] feat(api): tweaks to api to streamline vulkan object initialization --- src/api/api.zig | 9 +++++-- src/api/base.zig | 26 +++++++++++++----- src/api/command_buffer.zig | 6 ++--- src/api/queue.zig | 16 +++-------- src/api/sync.zig | 54 +++++++++++++++++++++++++++++++++++++ src/context.zig | 55 +++++++++++++++++++++++++++++++++++--- 6 files changed, 139 insertions(+), 27 deletions(-) create mode 100644 src/api/sync.zig diff --git a/src/api/api.zig b/src/api/api.zig index 06ed534..9fa5ee2 100644 --- a/src/api/api.zig +++ b/src/api/api.zig @@ -2,6 +2,7 @@ pub const vk = @import("vulkan"); const base = @import("base.zig"); const queue = @import("queue.zig"); +const sync = @import("sync.zig"); const buf = @import("buffer.zig"); const ind_buf = @import("index_buffer.zig"); const sh = @import("shader.zig"); @@ -12,8 +13,6 @@ const vert_buf = @import("vertex_buffer.zig"); pub const GlobalInterface = vk.BaseWrapper; pub const InstanceInterface = vk.InstanceProxy; pub const DeviceInterface = vk.DeviceProxy; -pub const Semaphore = vk.Semaphore; -pub const Fence = vk.Fence; pub const DynamicState = vk.DynamicState; /// All registered extensions for devices and instances @@ -26,6 +25,8 @@ pub const SurfaceHandler = base.SurfaceHandler; pub const GraphicsQueue = queue.GraphicsQueue; pub const ComputeQueue = queue.ComputeQueue; pub const PresentQueue = queue.PresentQueue; +pub const GenericQueue = queue.GenericQueue; +pub const QueueType = queue.QueueFamily; pub const Swapchain = @import("swapchain.zig"); pub const FrameBuffer = @import("frame_buffer.zig"); @@ -51,6 +52,10 @@ pub const BufInterface = buf.AnyBuffer; pub const Descriptor = @import("descriptor.zig"); pub const ResolvedDescriptorBinding = Descriptor.ResolvedBinding; +// sync stuff +pub const Semaphore = sync.Semaphore; +pub const Fence = sync.Fence; // shaders pub const ShaderModule = sh.Module; + diff --git a/src/api/base.zig b/src/api/base.zig index be2a051..1c34cba 100644 --- a/src/api/base.zig +++ b/src/api/base.zig @@ -4,10 +4,12 @@ const vk = @import("vulkan"); const std = @import("std"); const glfw = @import("glfw"); const util = @import("../util.zig"); +const api = @import("api.zig"); const Allocator = std.mem.Allocator; -const QueueFamily = @import("queue.zig").QueueFamily; +const QueueType = api.QueueType; +const Queue = api.ComputeQueue; const CommandBuffer = @import("command_buffer.zig"); pub const GetProcAddrHandler = *const (fn ( @@ -323,6 +325,9 @@ pub const DeviceHandler = struct { } ctx: *const InstanceHandler, + + // TODO: Support overlap in queue families + // (i.e) some queues might support both compute and graphics operations families: FamilyIndices, swapchain_details: SwapchainSupportDetails, @@ -374,6 +379,7 @@ pub const DeviceHandler = struct { var found_indices: FamilyIndices = .{ .graphics_family = null, .present_family = null, + .compute_family = null, }; const dev_queue_family_props = @@ -392,6 +398,11 @@ pub const DeviceHandler = struct { })) { found_indices.graphics_family = i; } + if (props.queue_flags.contains(.{ + .compute_bit = true, + })) { + found_indices.compute_family = i; + } if ((pr_inst.getPhysicalDeviceSurfaceSupportKHR( dev, @@ -414,7 +425,6 @@ pub const DeviceHandler = struct { _ = ctx; return if (a.val < b.val) .lt else if (a.val > b.val) .gt else .eq; } - //HACK: Since most modern GPUS are all but guarunteed to support what I'm doing, //I'm just going to pick the first discrete GPU and call it a day... @@ -554,14 +564,18 @@ pub const DeviceHandler = struct { return self.ctx.pr_inst.getPhysicalDeviceMemoryProperties(self.h_pdev); } - pub fn getQueueHandle(self: *const DeviceHandler, family: QueueFamily) ?vk.Queue { - const family_index = switch (family) { + pub fn getQueue( + self: *const DeviceHandler, + comptime family: QueueType, + ) ?api.GenericQueue(family) { + const index = switch (family) { .Graphics => self.families.graphics_family orelse return null, - .Present => self.families.present_family orelse return null, .Compute => self.families.compute_family orelse return null, + .Present => self.families.present_family orelse return null, }; - return self.pr_dev.getDeviceQueue(family_index, 0); + const handle = self.pr_dev.getDeviceQueue(index, 0); + return api.GenericQueue(family).fromHandle(self, handle); } pub fn draw( diff --git a/src/api/command_buffer.zig b/src/api/command_buffer.zig index f1865e7..e1b911d 100644 --- a/src/api/command_buffer.zig +++ b/src/api/command_buffer.zig @@ -41,7 +41,6 @@ fn initDev(dev: *const DeviceHandler) !Self { .h_cmd_pool = dev.h_cmd_pool, .dev = dev, }; - } pub fn oneShot(dev: *const DeviceHandler) !Self { @@ -77,11 +76,12 @@ pub fn end(self: *const Self) !void { // Also, synchronization is not gonna be handled yet... // the best way to handle synchronization is to only do 1 thing at a time 😊 // (by waiting idle) - + // We need queue handles from the context straight up, no way around it ugh // this shit is too bad to handle otherwise if (self.one_shot) { - const submit_queue = try GraphicsQueue.initDev(self.dev); + const submit_queue = self.dev.getQueue(.Graphics) orelse + return error.OneShotSubmitFailed; try submit_queue.submit(self, null, null, null); submit_queue.waitIdle(); } diff --git a/src/api/queue.zig b/src/api/queue.zig index 8a036ba..5bfadfc 100644 --- a/src/api/queue.zig +++ b/src/api/queue.zig @@ -25,26 +25,16 @@ pub fn GenericQueue(comptime p_family: QueueFamily) type { h_queue: vk.Queue, pr_dev: *const vk.DeviceProxy, - pub fn initDev(dev: *const DeviceHandler) !Self { - const queue_handle: vk.Queue = dev.getQueueHandle(family) orelse { - log.debug("Failed to acquire Queue handle", .{}); - return error.MissingQueueHandle; - }; + pub fn fromHandle(dev: *const DeviceHandler, h: vk.Queue) Self { return .{ - .h_queue = queue_handle, + .h_queue = h, .pr_dev = &dev.pr_dev, }; - } + } - pub fn init(ctx: *const Context) !Self { - const dev: *const DeviceHandler = ctx.env(.dev); - return initDev(dev); - } pub fn deinit(self: *Self) void { - // TODO: Annihilate queue - _ = self; } diff --git a/src/api/sync.zig b/src/api/sync.zig new file mode 100644 index 0000000..c48ac7f --- /dev/null +++ b/src/api/sync.zig @@ -0,0 +1,54 @@ +//!Helpful wrappers for GPU syncrhonization + +const std = @import("std"); +const api = @import("api.zig"); +const vk = @import("vulkan"); +const Context = @import("../context.zig"); + +pub const Semaphore = struct { + pr_dev: *const api.DeviceInterface, + h_sem: vk.Semaphore, + pub fn init(ctx: *const Context) !Semaphore { + const pr_dev: *const api.DeviceInterface = ctx.env(.di); + return .{ + .h_sem = try pr_dev.createSemaphore(&.{}, null), + .pr_dev = pr_dev, + }; + } + + pub fn deinit(self: *const Semaphore) void { + self.pr_dev.destroySemaphore(self.h_sem, null); + } +}; + +pub const Fence = struct { + pr_dev: *const api.DeviceInterface, + h_fence: vk.Fence, + pub fn init(ctx: *const Context, start_signaled: bool) !Fence { + const pr_dev: *const api.DeviceInterface = ctx.env(.di); + return .{ + .h_fence = try pr_dev.createFence(&.{ + .flags = .{ + .signaled_bit = start_signaled, + }, + }, null), + .pr_dev = pr_dev, + }; + } + + pub fn wait(self: *const Fence) !void { + _ = try self.pr_dev.waitForFences(1, &.{ + self.h_fence, + }, vk.TRUE, std.math.maxInt(u64)); + } + + pub fn reset(self: *const Fence) !void { + try self.pr_dev.resetFences(1, &.{ + self.h_fence + }); + } + + pub fn deinit(self: *const Fence) void { + self.pr_dev.destroyFence(self.h_fence, null); + } +}; diff --git a/src/context.zig b/src/context.zig index d5bde97..10f088c 100644 --- a/src/context.zig +++ b/src/context.zig @@ -1,6 +1,6 @@ const std = @import("std"); const api = @import("api/api.zig"); - +const vk = @import("vulkan"); const glfw = @import("glfw"); const e = @import("env.zig"); @@ -11,11 +11,14 @@ const ExtensionNameList = std.ArrayList([*:0]const u8); const Device = api.DeviceHandler; const Instance = api.InstanceHandler; const Surface = api.SurfaceHandler; +const CommandBuffer = api.CommandBuffer; +const Swapchain = api.Swapchain; + +const QueueType = api.QueueType; const GlobalInterface = api.GlobalInterface; const InstanceInterface = api.InstanceInterface; const DeviceInterface = api.DeviceInterface; -const VulkanAPI = api.VulkanAPI; const Ref = e.Ref; const EnvBacking = struct { @@ -41,6 +44,10 @@ global_interface: *const GlobalInterface, inst_interface: *const InstanceInterface, dev_interface: *const DeviceInterface, +graphics_queue: api.GraphicsQueue, +present_queue: api.PresentQueue, +compute_queue: api.ComputeQueue, + // NOTE: Planning on centralizing the entire vulkan API into a single struct for better // management, but not needed for now // vk_api: VulkanAPI, // heap allocated cuz big @@ -69,7 +76,7 @@ fn ResolveEnvType(comptime field: anytype) type { /// /// ## Extras /// * void -> entire environment (useful for scoping in API types) -/// +/// /// ## Usage Tips: /// * I STRONGLY recommend using manual type annotation if you want any hints from ZLS whatsoever /// because zls doesn't really handle comptime stuff very well yet. @@ -155,6 +162,11 @@ pub fn init(allocator: Allocator, config: Config) !*Self { .surface = &new.surf, }); errdefer new.dev.deinit(); + + new.compute_queue = new.dev.getQueue(.Compute) orelse return error.InvalidQueue; + new.graphics_queue = new.dev.getQueue(.Graphics) orelse return error.InvalidQueue; + new.present_queue = new.dev.getQueue(.Present) orelse return error.InvalidQueue; + new.allocator = allocator; // link references together @@ -176,3 +188,40 @@ pub fn deinit(self: *Self) void { const alloc = self.allocator; alloc.destroy(self); } + +pub const SyncInfo = struct { + fence_sig: ?vk.Fence = null, + fence_wait: ?vk.Fence = null, + + sem_sig: ?vk.Semaphore = null, + sem_wait: ?vk.Semaphore = null, +}; + +pub fn submitCommands( + self: *Self, + cmd_buf: *const CommandBuffer, + comptime queue_family: QueueType, + sync: SyncInfo, +) !void { + const queue = switch (queue_family) { + .Graphics => self.graphics_queue, + .Compute => self.present_queue, + .Present => self.present_queue, + }; + + try queue.submit( + cmd_buf, + sync.sem_wait, + sync.sem_sig, + sync.fence_wait, + ); +} + +pub fn presentFrame( + self: *Self, + swapchain: *const Swapchain, + sync: SyncInfo, +) !void { + const image = swapchain.image_index; + try self.present_queue.present(swapchain, image, sync.sem_wait); +} From 1a7a15c67d69c3b5da4d61a4f887298c11cf1117 Mon Sep 17 00:00:00 2001 From: johnnyboi12 <54293281+JohnSmoit@users.noreply.github.com> Date: Fri, 25 Jul 2025 19:16:19 -0600 Subject: [PATCH 11/22] feat(sample): compute sample now displays a fullscreen quad (will be used for further rendering --- samples/common/render_quad.zig | 10 ++++-- samples/slime/main.zig | 60 ++++++++++++++++++++++++++++------ 2 files changed, 57 insertions(+), 13 deletions(-) diff --git a/samples/common/render_quad.zig b/samples/common/render_quad.zig index fda5dbb..61488bb 100644 --- a/samples/common/render_quad.zig +++ b/samples/common/render_quad.zig @@ -38,11 +38,15 @@ const hardcoded_vert_src: []const u8 = \\ vec2(1.0, 1.0), \\ vec2(0.0, 1.0) \\); + \\ uint ind[6] = uint[]( + \\ 0, 1, 2, 0, 2, 3 + \\); \\ layout(location = 0) out vec2 texCoord; \\ \\ void main() { - \\ gl_Position = vec4(verts[gl_VertexIndex], 0.0, 1.0); - \\ texCoord = uvs[gl_VertexIndex]; + \\ uint index = ind[gl_VertexIndex]; + \\ gl_Position = vec4(verts[index], 0.0, 1.0); + \\ texCoord = uvs[index]; \\ } ; @@ -111,7 +115,7 @@ pub fn drawOneShot(self: *const Self, cmd_buf: *const CommandBuffer, framebuffer self.pipeline.bind(cmd_buf); const image_index = self.swapchain.image_index; self.renderpass.begin(cmd_buf, framebuffer, image_index); - self.dev.draw(cmd_buf, 4, 0, 0, 0); + self.dev.draw(cmd_buf, 6, 1, 0, 0); self.renderpass.end(cmd_buf); } diff --git a/samples/slime/main.zig b/samples/slime/main.zig index c76addc..6dc9f3e 100644 --- a/samples/slime/main.zig +++ b/samples/slime/main.zig @@ -24,6 +24,9 @@ const Descriptor = api.Descriptor; const DescriptorBinding = api.ResolvedDescriptorBinding; const CommandBuffer = api.CommandBuffer; +const Semaphore = api.Semaphore; +const Fence = api.Fence; + const GraphicsQueue = api.GraphicsQueue; const PresentQueue = api.PresentQueue; @@ -79,6 +82,18 @@ const GPUState = struct { //particles: StorageBuffer, }; +const SyncState = struct { + sem_render: Semaphore, + sem_acquire_frame: Semaphore, + frame_fence: Fence, + + pub fn deinit(self: *const SyncState) void { + self.sem_render.deinit(); + self.sem_acquire_frame.deinit(); + self.frame_fence.deinit(); + } +}; + const SampleState = struct { ctx: *Context = undefined, swapchain: Swapchain = undefined, @@ -88,8 +103,7 @@ const SampleState = struct { graphics: GraphicsState = undefined, gpu_state: GPUState = undefined, - present_queue: PresentQueue = undefined, - graphics_queue: GraphicsQueue = undefined, + sync: SyncState = undefined, pub fn createContext(self: *SampleState) !void { self.window = try helpers.makeBasicWindow(900, 600, "BAD APPLE >:}"); @@ -109,12 +123,6 @@ const SampleState = struct { .format = .r8g8b8a8_srgb, }, }); - - } - - pub fn retrieveDeviceQueues(self: *SampleState) !void { - self.graphics_queue = try api.GraphicsQueue.init(self.ctx); - self.present_queue = try api.PresentQueue.init(self.ctx); } pub fn createGraphicsPipeline(self: *SampleState) !void { @@ -141,6 +149,12 @@ const SampleState = struct { self.graphics.cmd_buf = try CommandBuffer.init(self.ctx); } + pub fn createSyncObjects(self: *SampleState) !void { + self.sync.frame_fence = try Fence.init(self.ctx, true); + self.sync.sem_acquire_frame = try Semaphore.init(self.ctx); + self.sync.sem_render = try Semaphore.init(self.ctx); + } + pub fn active(self: *const SampleState) bool { return !self.window.shouldClose(); } @@ -149,6 +163,12 @@ const SampleState = struct { pub fn update(self: *SampleState) !void { glfw.pollEvents(); + // wait for v + try self.sync.frame_fence.wait(); + try self.sync.frame_fence.reset(); + + _ = try self.swapchain.getNextImage(self.sync.sem_acquire_frame.h_sem, null); + try self.graphics.cmd_buf.reset(); try self.graphics.cmd_buf.begin(); self.graphics.render_quad.drawOneShot( @@ -157,12 +177,27 @@ const SampleState = struct { ); try self.graphics.cmd_buf.end(); + + // submit the command buffer to a synchronized queue + try self.ctx.submitCommands(&self.graphics.cmd_buf, .Graphics, .{ + .fence_wait = self.sync.frame_fence.h_fence, + .sem_sig = self.sync.sem_render.h_sem, + .sem_wait = self.sync.sem_acquire_frame.h_sem, + }); + + try self.ctx.presentFrame(&self.swapchain, .{ + .sem_wait = self.sync.sem_render.h_sem, + }); } pub fn deinit(self: *SampleState) void { + self.ctx.dev.waitIdle() catch {}; + self.graphics.deinit(); self.swapchain.deinit(); + + self.sync.deinit(); self.ctx.deinit(); self.window.destroy(); } @@ -178,13 +213,18 @@ pub fn main() !void { }; try state.createContext(); - try state.retrieveDeviceQueues(); + try state.createSyncObjects(); try state.createSwapchain(); try state.createGraphicsPipeline(); state.window.show(); while (state.active()) { - try state.update(); + state.update() catch |err| { + log.err("An error occured while running: {!}\n ....Terminating", .{err}); + state.deinit(); + + return err; + }; } state.deinit(); From 4e705c35af9bfc2a39168a275740d94655d0da70 Mon Sep 17 00:00:00 2001 From: johnnyboi12 <54293281+JohnSmoit@users.noreply.github.com> Date: Sat, 26 Jul 2025 09:19:42 -0600 Subject: [PATCH 12/22] feat(sample): added uniforms to the slime sample --- samples/common/render_quad.zig | 7 +++++++ samples/slime/frag.glsl | 11 ++++++++++- samples/slime/main.zig | 35 ++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/samples/common/render_quad.zig b/samples/common/render_quad.zig index 61488bb..7233b9f 100644 --- a/samples/common/render_quad.zig +++ b/samples/common/render_quad.zig @@ -23,6 +23,7 @@ pipeline: GraphicsPipeline = undefined, renderpass: RenderPass = undefined, dev: *const DeviceHandler = undefined, swapchain: *const Swapchain = undefined, +desc: ?*const Descriptor = null, const hardcoded_vert_src: []const u8 = \\ #version 450 @@ -109,12 +110,18 @@ pub fn initSelf(self: *Self, ctx: *const Context, allocator: Allocator, config: self.dev = ctx.env(.dev); self.swapchain = config.swapchain; + self.desc = config.frag_descriptors; } pub fn drawOneShot(self: *const Self, cmd_buf: *const CommandBuffer, framebuffer: *const FrameBuffer) void { self.pipeline.bind(cmd_buf); const image_index = self.swapchain.image_index; self.renderpass.begin(cmd_buf, framebuffer, image_index); + + if (self.desc) |d| { + d.bind(cmd_buf, self.pipeline.h_pipeline_layout); + } + self.dev.draw(cmd_buf, 6, 1, 0, 0); self.renderpass.end(cmd_buf); } diff --git a/samples/slime/frag.glsl b/samples/slime/frag.glsl index 74b7630..cc7c518 100644 --- a/samples/slime/frag.glsl +++ b/samples/slime/frag.glsl @@ -3,6 +3,15 @@ layout(location = 0) out vec4 fragColor; layout(location = 0) in vec2 texCoord; +layout(binding = 0) uniform SampleUniforms { + float time; + vec2 mouse; +} uniforms; + void main() { - fragColor = vec4(texCoord, 0.0, 1.0); + vec2 ot = vec2( + min(1.0, texCoord.x + sin(uniforms.time)), + min(1.0, texCoord.y + cos(uniforms.time)) + ); + fragColor = vec4(ot, 0.0, 1.0); } diff --git a/samples/slime/main.zig b/samples/slime/main.zig index 6dc9f3e..9e7e2ed 100644 --- a/samples/slime/main.zig +++ b/samples/slime/main.zig @@ -37,6 +37,7 @@ const Compute = api.Compute; const log = std.log.scoped(.application); const GraphicsState = struct { + descriptor: Descriptor, framebuffers: FrameBuffer, render_quad: RenderQuad, @@ -52,11 +53,13 @@ const GraphicsState = struct { self.render_quad.deinit(); self.framebuffers.deinit(); self.cmd_buf.deinit(); + self.descriptor.deinit(); } }; const ApplicationUniforms = extern struct { time: f32, + mouse: math.Vec2, }; const GPUState = struct { @@ -80,6 +83,10 @@ const GPUState = struct { // compute visible only // -- contains simulation agents //particles: StorageBuffer, + + pub fn deinit(self: *GPUState) void { + self.uniforms.buffer().deinit(); + } }; const SyncState = struct { @@ -135,8 +142,28 @@ const SampleState = struct { defer frag_shader.deinit(); + // initialize uniforms + self.gpu_state.host_uniforms = .{ + .time = 0, + .mouse = math.vec(.{ 0, 0 }), + }; + + self.gpu_state.uniforms = try UniformBuffer(ApplicationUniforms) + .create(self.ctx); + + try self.gpu_state.uniforms.buffer().setData(&self.gpu_state.host_uniforms); + + // create fragment-specific descriptors + self.graphics.descriptor = try Descriptor.init(self.ctx, self.allocator, .{ + .bindings = &.{.{ + .data = .{ .Uniform = self.gpu_state.uniforms.buffer() }, + .stages = .{ .fragment_bit = true }, + }}, + }); + try self.graphics.render_quad.initSelf(self.ctx, self.allocator, .{ .frag_shader = &frag_shader, + .frag_descriptors = &self.graphics.descriptor, .swapchain = &self.swapchain, }); @@ -159,6 +186,11 @@ const SampleState = struct { return !self.window.shouldClose(); } + fn updateUniforms(self: *SampleState) !void { + self.gpu_state.host_uniforms.time = @floatCast(glfw.getTime()); + try self.gpu_state.uniforms.buffer().setData(&self.gpu_state.host_uniforms); + } + // intercepts errors and logs them pub fn update(self: *SampleState) !void { glfw.pollEvents(); @@ -167,6 +199,8 @@ const SampleState = struct { try self.sync.frame_fence.wait(); try self.sync.frame_fence.reset(); + try self.updateUniforms(); + _ = try self.swapchain.getNextImage(self.sync.sem_acquire_frame.h_sem, null); try self.graphics.cmd_buf.reset(); @@ -194,6 +228,7 @@ const SampleState = struct { self.ctx.dev.waitIdle() catch {}; self.graphics.deinit(); + self.gpu_state.deinit(); self.swapchain.deinit(); From 0ac2ce5c04ede56d51cd32f0751443247287c527 Mon Sep 17 00:00:00 2001 From: johnnyboi12 <54293281+JohnSmoit@users.noreply.github.com> Date: Tue, 29 Jul 2025 09:46:01 -0600 Subject: [PATCH 13/22] feat(sample): implement hello compute with storage image reading/writing --- samples/slime/frag.glsl.spv | Bin 568 -> 0 bytes samples/slime/main.zig | 59 +++++++++++++++++++---- samples/slime/shaders/compute_map.glsl | 0 samples/slime/shaders/compute_slime.glsl | 51 ++++++++++++++++++++ samples/slime/{ => shaders}/frag.glsl | 0 5 files changed, 101 insertions(+), 9 deletions(-) delete mode 100644 samples/slime/frag.glsl.spv create mode 100644 samples/slime/shaders/compute_map.glsl create mode 100644 samples/slime/shaders/compute_slime.glsl rename samples/slime/{ => shaders}/frag.glsl (100%) diff --git a/samples/slime/frag.glsl.spv b/samples/slime/frag.glsl.spv deleted file mode 100644 index bc8bbf92c9396b0428dbda266f748a27ef40cd5b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 568 zcmYk2PfNo<6vUrR)7IAhv!FMrcod2UErO_rBDn;r_yIy`q6A`7(kOcKv-zpK2+nV9 zr3)`_XLk0@Y`n@z-E7BN*0zEDt-e*nn7EqjLHHPsR%JXozc@qDG0zFnbggbxb?Rl7 zE;!g#?Wuaw!OK89PgBjlbg61h3&JRvPJ{3+nlG2ps{F{3C`%Vf6sJWpFVnXKU1Lj^ zF6P;5ye;sh*!K0!civCM?0J%Bc_BYIvXsgDB+rXjTMKlHj7I=1Fq#b>C@EQwU21dt z1MO?#hN?s9c@r4_p}q}u{tEon8sha97of3Cmg@Vr-YvhSB Ct1q+w diff --git a/samples/slime/main.zig b/samples/slime/main.zig index 9e7e2ed..14e4f27 100644 --- a/samples/slime/main.zig +++ b/samples/slime/main.zig @@ -31,11 +31,21 @@ const GraphicsQueue = api.GraphicsQueue; const PresentQueue = api.PresentQueue; const UniformBuffer = api.ComptimeUniformBuffer; +const StorageBuffer = api.ComptimeStorageBuffer; const TexImage = api.Image; const Compute = api.Compute; const log = std.log.scoped(.application); +const ComputeState = struct { + // pipeline for running slime simulation + slime_pipeline: Compute, + // pipeline for updating pheremone map + stinky_pipeline: Compute, + + cmd_buf: CommandBuffer, +}; + const GraphicsState = struct { descriptor: Descriptor, framebuffers: FrameBuffer, @@ -43,12 +53,6 @@ const GraphicsState = struct { cmd_buf: CommandBuffer, - // pipeline for running slime simulation - slime_pipeline: Compute, - - // pipeline for updating pheremone map - stinky_pipeline: Compute, - pub fn deinit(self: *GraphicsState) void { self.render_quad.deinit(); self.framebuffers.deinit(); @@ -62,6 +66,10 @@ const ApplicationUniforms = extern struct { mouse: math.Vec2, }; +const Particle = extern struct { + position: math.Vec4, +}; + const GPUState = struct { host_uniforms: ApplicationUniforms, @@ -82,8 +90,8 @@ const GPUState = struct { // compute visible only // -- contains simulation agents - //particles: StorageBuffer, - + particles: StorageBuffer(Particle), + pub fn deinit(self: *GPUState) void { self.uniforms.buffer().deinit(); } @@ -108,6 +116,7 @@ const SampleState = struct { allocator: Allocator, graphics: GraphicsState = undefined, + compute: ComputeState = undefined, gpu_state: GPUState = undefined, sync: SyncState = undefined, @@ -136,7 +145,7 @@ const SampleState = struct { const frag_shader = try helpers.initSampleShader( self.ctx, self.allocator, - "slime/frag.glsl", + "slime/shaders/frag.glsl", .Fragment, ); @@ -180,6 +189,38 @@ const SampleState = struct { self.sync.frame_fence = try Fence.init(self.ctx, true); self.sync.sem_acquire_frame = try Semaphore.init(self.ctx); self.sync.sem_render = try Semaphore.init(self.ctx); + + //compute sync objects + } + + pub fn createComputePipelines(self: *SampleState) !void { + self.compute.cmd_buf = try CommandBuffer.init(self.ctx); + + const shader = try helpers.initSampleShader( + self.ctx, + self.allocator, + "slime/shaders/compute_slime.glsl", + .Compute, + ); + + const descriptors: []const DescriptorBinding = &.{ + .{ + .data = .{ .Uniform = self.gpu_state.compute_uniforms.buffer() }, + .stages = .{ .compute_bit = true }, + }, + .{ + .data = .{ .StorageBuffer = self.gpu_state.particles.buffer() }, + .stages = .{ .compute_bit = true }, + }, + .{ + .data = .{}, + .stages = .{ .compute_bit = true }, + }, + }; + self.compute.slime_pipeline = try Compute.init(self.ctx, self.allocator, .{ + .shader = &shader, + .desc_bindings = descriptors, + }); } pub fn active(self: *const SampleState) bool { diff --git a/samples/slime/shaders/compute_map.glsl b/samples/slime/shaders/compute_map.glsl new file mode 100644 index 0000000..e69de29 diff --git a/samples/slime/shaders/compute_slime.glsl b/samples/slime/shaders/compute_slime.glsl new file mode 100644 index 0000000..73672e4 --- /dev/null +++ b/samples/slime/shaders/compute_slime.glsl @@ -0,0 +1,51 @@ +#version 450 + + +struct Particle { + vec4 position; +}; + +//TODO: Implement PUSH constants NOW +layout(binding = 0) uniform UniformBuffer { + vec3 col; + uvec2 res; + uint particle_count; + uint pixels_rad; +} uniforms; + +// buffer of particles +layout(std140, binding = 1) buffer ParticlesBuffer{ + Particle particles[]; +} agents; + +// output image +//TODO: Needs new descriptor type: Storage image +layout(rgba8_snorm, binding = 2) writeonly image2D output; + +layout(local_size_x = 8, local_size_y = 8) in; + +// just write the particles positions to an output image I guess... +void main() { + if (gl_GlobalInvocationID.x > uniforms.particle_count) + return; + + ivec2 pos = ivec2( + int(agents.particles[gl_GlobalInvocationID.x].position.x), + int(agents.particles[gl_GlobalInvocationID.x].position.y) + ); + + vec2 xb = vec2( + max(0, pos.x - uniforms.pixels_rad), + min(uniforms.res, pos.x + uniforms.pixels_rad) + ); + vec2 yb = vec2( + max(0, pos.y - uniforms.pixels_rad), + min(uniforms.res, pos.y + uniforms.pixels_rad) + ); + + for (int x = xb.x; x < xb.y; x++) { + for (int y = yb.x; y < yb.y; y++) { + imageStore(output, ivec2(x, y), vec4(col, 1.0)); + } + } +} diff --git a/samples/slime/frag.glsl b/samples/slime/shaders/frag.glsl similarity index 100% rename from samples/slime/frag.glsl rename to samples/slime/shaders/frag.glsl From 9d579b3239da05f393a465f1338b48cb84245c68 Mon Sep 17 00:00:00 2001 From: johnnyboi12 <54293281+JohnSmoit@users.noreply.github.com> Date: Tue, 29 Jul 2025 09:46:35 -0600 Subject: [PATCH 14/22] feat(vulkan): update images and buffers to support storage usage --- src/api/api.zig | 3 +- src/api/buffer.zig | 36 ++++++++- src/api/compute.zig | 23 +----- src/api/image.zig | 155 +++++++++++++++++++++++++++---------- src/api/storage_buffer.zig | 73 +++++++++++++++++ 5 files changed, 224 insertions(+), 66 deletions(-) create mode 100644 src/api/storage_buffer.zig diff --git a/src/api/api.zig b/src/api/api.zig index 9fa5ee2..5afab51 100644 --- a/src/api/api.zig +++ b/src/api/api.zig @@ -47,7 +47,8 @@ pub const TexImage = @import("texture.zig"); pub const ComptimeVertexBuffer = vert_buf.VertexBuffer; pub const ComptimeIndexBuffer = ind_buf.IndexBuffer; pub const ComptimeUniformBuffer = uni_buf.UniformBuffer; -pub const BufInterface = buf.AnyBuffer; +pub const ComptimeStorageBuffer = @import("storage_buffer.zig").ComptimeStorageBuffer; +const BufInterface = buf.AnyBuffer; pub const Descriptor = @import("descriptor.zig"); pub const ResolvedDescriptorBinding = Descriptor.ResolvedBinding; diff --git a/src/api/buffer.zig b/src/api/buffer.zig index f027d9c..387730d 100644 --- a/src/api/buffer.zig +++ b/src/api/buffer.zig @@ -65,16 +65,46 @@ pub const AnyBuffer = struct { /// Ease-of-use mixins for creating opaque vtable functions /// TODO: make this -pub fn BindMixin() BindFn { +pub fn BindMixin(comptime T: type, val: ?*const (fn(*T, *const CommandBuffer) void)) BindFn { + const Wrapper = struct { + pub fn f(ctx: *anyopaque, cmd_buf: *const CommandBuffer) void { + const self: *T = @ptrCast(@alignCast(ctx)); + if (val) |func| func(self, cmd_buf); + } + }; + + return Wrapper.f; +} + +pub fn SetDataMixin(comptime T: type, val: ?*const (fn(*T, *anyopaque) anyerror!void)) SetDataFn { + const Wrapper = struct { + pub fn f(ctx: *anyopaque, data: *anyopaque) anyerror!void { + const self: *T = @ptrCast(@alignCast(ctx)); + if (val) |func| try func(self, data); + } + }; + return Wrapper.f; } -pub fn SetDataMixin() SetDataFn { +pub fn DeinitMixin(comptime T: type, val: ?*const (fn(*T) void)) DeinitFn { + const Wrapper = struct { + pub fn f(ctx: *anyopaque) void { + const self: *T = @ptrCast(@alignCast(ctx)); + if (val) |func| func(self); + } + }; + return Wrapper.f; } -pub fn DeinitMixin() DeinitFn { +pub fn AutoVTable(comptime T: type) *const VTable { + const info = @typeInfo(T); + const sinfo = switch(info) { + .Struct => |s| s, + else => @compileError("Invalid Vtable base type, must be struct"), + }; } pub fn copy(src: AnyBuffer, dst: AnyBuffer, dev: *const DeviceHandler) !void { diff --git a/src/api/compute.zig b/src/api/compute.zig index 9e2d9db..fa3877e 100644 --- a/src/api/compute.zig +++ b/src/api/compute.zig @@ -25,22 +25,6 @@ h_pipeline: vk.Pipeline, h_pipeline_layout: vk.PipelineLayout, desc: Descriptor, -pub fn fromShaderFileAlloc( - ctx: *const Context, - allocator: Allocator, - path: []const u8, -) !Self { - const shader = try ShaderModule.fromSourceFile(ctx, allocator, .{ - .filename = path, - .stage = .Compute, - }); - defer shader.deinit(); - - return init(ctx, .{ - .shader = shader, - }); -} - pub fn init(ctx: *const Context, allocator: Allocator, cfg: Config) !Self { const pr_dev: *const DeviceInterface = ctx.env(.di); const desc = try Descriptor.init(ctx, allocator, &.{ @@ -52,7 +36,7 @@ pub fn init(ctx: *const Context, allocator: Allocator, cfg: Config) !Self { const layout = try pr_dev.createPipelineLayout(&.{ .flags = .{}, .set_layout_count = 1, - .p_set_layouts = util.asManyPtr(vk.DescriptorSetLayout, &desc.h_desc_layout), + .p_set_layouts = &.{&desc.h_desc_layout}, }, null); var new = Self{ @@ -71,10 +55,7 @@ pub fn init(ctx: *const Context, allocator: Allocator, cfg: Config) !Self { try pr_dev.createComputePipelines( .null_handle, 1, - util.asManyPtr( - vk.ComputePipelineCreateInfo, - &compute_pipeline_info, - ), + &.{&compute_pipeline_info}, null, @as([*]vk.Pipeline, @ptrCast(&new.h_pipeline)), ); diff --git a/src/api/image.zig b/src/api/image.zig index 2a8483a..07a09c7 100644 --- a/src/api/image.zig +++ b/src/api/image.zig @@ -46,16 +46,16 @@ format: vk.Format = .undefined, pub const Config = struct { usage: vk.ImageUsageFlags, format: vk.Format, - tiling: vk.ImageTiling, mem_flags: vk.MemoryPropertyFlags, width: u32, height: u32, + + tiling: vk.ImageTiling = .linear, + clear_col: ?vk.ClearColorValue = null, staging_buf: ?*StagingBuffer = null, initial_layout: vk.ImageLayout = .undefined, }; -// NOTE: Yet another instance of a BAD function that allocates device memory in a non-zig like fashion -// -- memory allocator for device memory coming soon! fn createImageMemory( dev: *const DeviceHandler, img: vk.Image, @@ -108,13 +108,45 @@ pub fn createView(self: *const Self, aspect_mask: vk.ImageAspectFlags) !View { }; } -pub fn transitionLayout( +pub const AccessMapEntry = struct { + stage: vk.PipelineStageFlags = .{}, + access: vk.AccessFlags = .{}, +}; + +const transition_map = std.enums.EnumMap(vk.ImageLayout, AccessMapEntry).init(.{ + .undefined = .{ + .stage = .{ .top_of_pipe_bit = true }, + .access = .{}, + }, + .general = .{ + .stage = .{ .compute_shader_bit = true }, + .access = .{}, + }, + .transfer_dst_optimal = .{ + .stage = .{ .transfer_bit = true }, + .access = .{ .transfer_write_bit = true }, + }, + .shader_read_only_optimal = .{ + .stage = .{ .fragment_shader_bit = true }, + .access = .{ .shader_read_bit = true }, + }, +}); + +pub const LayoutTransitionOptions = struct { + cmd_buf: ?*const CommandBuffer, + access_overrides: ?vk.AccessFlags, +}; + +/// injects a layout transition command into an existing command buffer +/// barriers included. +/// Command buffer MUST be specified in opts +pub fn cmdTransitionLayout( self: *Self, from: vk.ImageLayout, to: vk.ImageLayout, -) !void { - const transition_cmds = try CommandBuffer.oneShot(self.dev); - defer transition_cmds.deinit(); + opts: LayoutTransitionOptions, +) void { + const cmd_buf = opts.cmd_buf orelse return; var transition_barrier = vk.ImageMemoryBarrier{ .old_layout = from, @@ -150,46 +182,50 @@ pub fn transitionLayout( // as far as specifying the pipeline stages the barrier sits between, as well as which parts // of the resource should be accessed as the source and destination of the barrier transition - var src_stage = vk.PipelineStageFlags{}; // pipeline stage to happen before the barrier - var dst_stage = vk.PipelineStageFlags{}; // pipeline stage to happen after the barrier + const src_properties = transition_map.get(from) orelse .{}; + const dst_properties = transition_map.get(to) orelse .{}; - if (from == .undefined and to == .transfer_dst_optimal) { - // Technically, you could be implicit about this, since command buffers implicitly include - // a .host_write_bit when submitted, but that's yucky - transition_barrier.src_access_mask = .{}; - transition_barrier.dst_access_mask = .{ .transfer_write_bit = true }; + const src_access = opts.access_overrides orelse .{}; + const dst_access = opts.access_overrides orelse .{}; - src_stage = .{ .top_of_pipe_bit = true }; - dst_stage = .{ .transfer_bit = true }; - } else if (from == .transfer_dst_optimal and to == .shader_read_only_optimal) { - transition_barrier.src_access_mask = .{ .transfer_write_bit = true }; - transition_barrier.dst_access_mask = .{ .shader_read_bit = true }; + const default_src_access = (transition_map.get(from) orelse .{}).access; + const default_dst_access = (transition_map.get(to) orelse .{}).access; - src_stage = .{ .transfer_bit = true }; - dst_stage = .{ .fragment_shader_bit = true }; - } else { - log.err("Not a supported layout transition (This function is not generalized for all transitions)", .{}); - return error.Fuck; - } + transition_barrier.src_access_mask = default_src_access.merge(src_access); + transition_barrier.src_access_mask = default_dst_access.merge(dst_access); self.dev.pr_dev.cmdPipelineBarrier( - transition_cmds.h_cmd_buffer, - src_stage, - dst_stage, + cmd_buf.h_cmd_buffer, + src_properties.stage, + dst_properties.stage, .{}, // allows for regional reading from the resource 0, null, 0, null, 1, - //TODO: Priority Uno -- This many pointer function is really cumbersome - many(vk.ImageMemoryBarrier, &transition_barrier), + &.{&transition_barrier}, ); +} - //TODO: one shot command buffers should auto submit when they end... - transition_cmds.end() catch |err| { - return err; +/// opts command buffer is ignored if specified +pub fn transitionLayout( + self: *Self, + from: vk.ImageLayout, + to: vk.ImageLayout, + opts: LayoutTransitionOptions, +) !void { + const transition_cmds = try CommandBuffer.oneShot(self.dev); + defer transition_cmds.deinit(); + + const opts2 = LayoutTransitionOptions{ + .access_overrides = opts.access_overrides, + .cmd_buf = transition_cmds, }; + + self.cmdTransitionLayout(from, to, opts2); + + try transition_cmds.end(); } fn copyFromStaging(self: *Self, staging_buf: *StagingBuffer, extent: vk.Extent3D) !void { @@ -224,7 +260,29 @@ fn copyFromStaging(self: *Self, staging_buf: *StagingBuffer, extent: vk.Extent3D }; } -fn init_self(self: *Self, dev: *const DeviceHandler, config: *const Config) !void { +pub fn cmdClear( + self: *Self, + col: vk.ClearColorValue, + cmd_buf: *const CommandBuffer, + cur_layout: vk.ImageLayout, +) void { + self.dev.pr_dev.cmdClearColorImage( + cmd_buf, + self.h_img, + cur_layout, + &.{col}, + 1, + &.{.{ + .aspect_mask = .{ .color_bit = true }, + .base_array_layer = 0, + .layer_count = 1, + .base_mip_level = 0, + .level_count = 1, + }}, + ); +} + +fn initSelf(self: *Self, dev: *const DeviceHandler, config: Config) !void { const image_info = vk.ImageCreateInfo{ .image_type = .@"2d", .extent = .{ @@ -263,26 +321,41 @@ fn init_self(self: *Self, dev: *const DeviceHandler, config: *const Config) !voi // 2. Transition from TRANSFER_DST_OPTIMAL to SHADER_READ_ONLY_OPTIMAL to prepare the image to be used a sampler // (Which obviously is accessed as read only from the shader using the sampler as an intermediary) - if (config.initial_layout == .undefined) { return; } if (config.staging_buf != null) { - try self.transitionLayout(.undefined, .transfer_dst_optimal); + try self.transitionLayout(.undefined, .transfer_dst_optimal, .{}); try self.copyFromStaging(config.staging_buf.?, image_info.extent); - try self.transitionLayout(.transfer_dst_optimal, config.initial_layout); + try self.transitionLayout(.transfer_dst_optimal, config.initial_layout, .{}); } else { - // NOTE: This disregards the fact that if a staging buffer is not used, then + // WARN: This disregards the fact that if a staging buffer is not used, then // the user is probably copying from host visible memory (not sure if this handles that // correctly) - try self.transitionLayout(.undefined, config.initial_layout); + const tmp_cmds = try CommandBuffer.oneShot(); + + if (config.clear_col) |col| { + self.cmdTransitionLayout(.undefined, .transfer_dst_optimal, .{ + .cmd_buf = &tmp_cmds, + }); + self.cmdClear(col, &tmp_cmds, .transfer_dst_optimal); + self.cmdTransitionLayout(.transfer_dst_optimal, config.initial_layout, .{ + .cmd_buf = &tmp_cmds, + }); + } else { + self.cmdTransitionLayout(.undefined, config.initial_layout, .{ + .cmd_buf = &tmp_cmds, + }); + } + + try tmp_cmds.end(); } } -pub fn init(ctx: *const Context, config: *const Config) !Self { +pub fn init(ctx: *const Context, config: Config) !Self { var image = Self{}; - try image.init_self(ctx.env(.dev), config); + try image.initSelf(ctx.env(.dev), config); return image; } diff --git a/src/api/storage_buffer.zig b/src/api/storage_buffer.zig new file mode 100644 index 0000000..bcd253c --- /dev/null +++ b/src/api/storage_buffer.zig @@ -0,0 +1,73 @@ +const api = @import("api.zig"); +const buf_api = @import("buffer.zig"); + +const GenericBuffer = buf_api.GenericBuffer; +const AnyBuffer = buf_api.AnyBuffer; +const Context = @import("../context.zig"); + +const CommandBuffer = api.CommandBuffer; + +pub fn ComptimeStorageBuffer(comptime T: type) type { + return struct { + const InnerType = buf_api.GenericBuffer(T, .{ + .memory = .{ .device_local_bit = true }, + .usage = .{ + .transfer_dst_bit = true, + .storage_buffer_bit = true, + }, + }); + const Self = @This(); + + buf: InnerType, + + pub fn create(ctx: *const Context, size: usize) !Self { + return .{ + .buf = try InnerType.create(ctx, size), + }; + } + + pub fn setData(ctx: *anyopaque, data: *const anyopaque) !void { + const self: *Self = @ptrCast(@alignCast(ctx)); + const elem: []const T = @as([*]const T, @ptrCast(@alignCast(data)))[0..self.buf.size]; + + var staging = try self.buf.createStaging(); + defer staging.deinit(); + + const staging_mem = try staging.mapMemory(); + defer staging.unmapMemory(); + + @memcpy(staging_mem, elem); + + try buf_api.copy(staging.buffer(), self.buffer(), self.buf.dev); + } + + pub fn bind(ctx: *anyopaque, cmd_buf: *const CommandBuffer) void { + const self: *Self = @ptrCast(@alignCast(ctx)); + self.buf.dev.pr_dev.cmdBindVertexBuffers( + cmd_buf.h_cmd_buffer, + 0, + 1, + &.{&self.buf.h_buf}, + &.{0}, + ); + } + pub fn deinit(ctx: *anyopaque) void { + const self: *Self = @ptrCast(@alignCast(ctx)); + self.buf.deinit(); + } + + pub fn buffer(self: *Self) AnyBuffer { + return AnyBuffer{ + .cfg = &InnerType.cfg, + .handle = self.buf.h_buf, + .ptr = self, + .size = self.buf.bytesSize(), + .vtable = &.{ + .bind = bind, + .setData = setData, + .deinit = deinit, + }, + }; + } + }; +} From 19f5b2f0998d4d9d137660eb431dc1bcaa349dc0 Mon Sep 17 00:00:00 2001 From: johnnyboi12 <54293281+JohnSmoit@users.noreply.github.com> Date: Tue, 29 Jul 2025 09:47:11 -0600 Subject: [PATCH 15/22] feat(misc): update math types and utilities to support an additional 4-comp vector --- src/math.zig | 25 ++++++++++++++++++++----- src/util.zig | 31 +++++++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/src/math.zig b/src/math.zig index 1997529..f05ce0b 100644 --- a/src/math.zig +++ b/src/math.zig @@ -45,6 +45,19 @@ pub const Vec3 = extern struct { } }; +pub const Vec4 = extern struct { + pub const len: usize = 4; + + x: f32, + y: f32, + z: f32, + w: f32, + + pub fn vals(self: Vec4) struct { f32, f32, f32, f32 } { + return .{ self.x, self.y, self.z, self.w }; + } +}; + pub const Vec2 = extern struct { pub const len: usize = 2; @@ -60,6 +73,7 @@ fn resolveVec(args: comptime_int) type { return switch (args) { 2 => Vec2, 3 => Vec3, + 4 => Vec4, else => @compileError("No corresponding vector type"), }; } @@ -70,6 +84,7 @@ pub fn vec(args: anytype) resolveVec(args.len) { return switch (args.len) { 2 => Vec2{ .x = args[0], .y = args[1] }, 3 => Vec3{ .x = args[0], .y = args[1], .z = args[2] }, + 4 => Vec4{ .x = args[0], .y = args[1], .z = args[2], .w = args[3] }, else => unreachable, }; } @@ -228,10 +243,10 @@ pub const Mat4 = extern struct { pub fn rotateY(mat: Mat4, rads: f32) Mat4 { return mat.mul(create(.{ - .{@cos(rads), 0, @sin(rads), 0}, - .{0, 1, 0, 0}, - .{-@sin(rads), 0, @cos(rads), 0}, - .{0, 0, 0, 1}, + .{ @cos(rads), 0, @sin(rads), 0 }, + .{ 0, 1, 0, 0 }, + .{ -@sin(rads), 0, @cos(rads), 0 }, + .{ 0, 0, 0, 1 }, })); } @@ -295,7 +310,7 @@ pub const Mat4 = extern struct { const ty = -dot(y, eye); const tz = -dot(z, eye); - return view.translate(vec(.{tx, ty, tz})); + return view.translate(vec(.{ tx, ty, tz })); } pub fn perspective(fov: f32, aspect: f32, near: f32, far: f32) Mat4 { diff --git a/src/util.zig b/src/util.zig index 154db9a..83eb030 100644 --- a/src/util.zig +++ b/src/util.zig @@ -1,3 +1,4 @@ +const std = @import("std"); pub fn asCString(rep: anytype) [*:0]const u8 { return @as([*:0]const u8, @ptrCast(rep)); } @@ -6,10 +7,16 @@ pub fn emptySlice(comptime T: type) []T { return &[0]T{}; } +//FIXME: This is due for a BIG REFACTOR COMING SOON +// (because it is much worse than just &.{} which I Didn't know was a thing oops. pub fn asManyPtr(comptime T: type, ptr: *const T) [*]const T { return @as([*]const T, @ptrCast(ptr)); } +//FIXME: This is due for a BIG REFACTOR COMING SOON +// (because it is much worse than just &.{} which I Didn't know was a thing oops. +// funilly enough, &.{1, 2, 3} is shorter than span(.{1, 2, 3}). I just don't +// like reading docs + idk how. pub fn span(v: anytype) [] @TypeOf(v[0]) { const T = @TypeOf(v[0]); comptime var sp: [v.len] T = undefined; @@ -20,6 +27,26 @@ pub fn span(v: anytype) [] @TypeOf(v[0]) { return sp[0..]; } +const StructInfo = std.builtin.Type.Struct; +const StructField = std.builtin.Type.StructField; +const Function = std.builtin.Type.Fn; -// TODO: Basic logging function that displays enclosing type for member functions -pub const Logger = struct {}; +// Reflection Stuff +pub fn tryGetField(info: *StructInfo, name: []const u8) ?*StructField { + for (info.fields) |*fld| { + if (std.mem.eql(u8, fld.name, name) == .eq) { + return fld; + } + } + + return null; +} + +pub fn signatureMatches(a: *const Function, b: *const Function) bool { + if (a.params.len != b.params.len) return false; + for (a.params, b.params) |p1, p2| + if (p1.type != p2.type) return false; + + if (!a.calling_convention.eql(b)) return false; + return true; +} From ed17fbbc8f7b326597444ef7d460858577e9f20d Mon Sep 17 00:00:00 2001 From: johnnyboi12 <54293281+JohnSmoit@users.noreply.github.com> Date: Fri, 1 Aug 2025 17:50:26 -0600 Subject: [PATCH 16/22] feat(sample): slime sample now has complete compute initialization --- samples/slime/main.zig | 57 ++++++++++++++++++++++-- samples/slime/shaders/compute_slime.glsl | 18 ++++---- 2 files changed, 62 insertions(+), 13 deletions(-) diff --git a/samples/slime/main.zig b/samples/slime/main.zig index 14e4f27..b8553d5 100644 --- a/samples/slime/main.zig +++ b/samples/slime/main.zig @@ -32,7 +32,7 @@ const PresentQueue = api.PresentQueue; const UniformBuffer = api.ComptimeUniformBuffer; const StorageBuffer = api.ComptimeStorageBuffer; -const TexImage = api.Image; +const Image = api.Image; const Compute = api.Compute; const log = std.log.scoped(.application); @@ -44,6 +44,11 @@ const ComputeState = struct { stinky_pipeline: Compute, cmd_buf: CommandBuffer, + + pub fn deinit(self: *ComputeState) void { + self.slime_pipeline.deinit(); + self.cmd_buf.deinit(); + } }; const GraphicsState = struct { @@ -66,6 +71,16 @@ const ApplicationUniforms = extern struct { mouse: math.Vec2, }; +const ComputeUniforms = extern struct { + col: math.Vec3, + res_x: u32, + res_y: u32, + particle_count: u32, + pixels_rad: u32, +}; + +const PARTICLE_COUNT = 1024; + const Particle = extern struct { position: math.Vec4, }; @@ -75,6 +90,7 @@ const GPUState = struct { // compute and graphics visible uniforms: UniformBuffer(ApplicationUniforms), + compute_uniforms: UniformBuffer(ComputeUniforms), // compute and graphics visible (needs viewport quad) // written to in the compute shader and simply mapped to the viewport @@ -82,18 +98,24 @@ const GPUState = struct { // ... This also represents the pheremone map that sim agents in the compute // shader would use to determine their movements // - render_target: TexImage, + render_target: Image, + render_view: Image.View, // compute visible only // (contains source image data to base simulation off of) - src_image: TexImage, + src_image: Image, // compute visible only // -- contains simulation agents particles: StorageBuffer(Particle), pub fn deinit(self: *GPUState) void { + self.particles.buffer().deinit(); self.uniforms.buffer().deinit(); + self.compute_uniforms.buffer().deinit(); + + self.render_view.deinit(); + self.render_target.deinit(); } }; @@ -202,6 +224,25 @@ const SampleState = struct { "slime/shaders/compute_slime.glsl", .Compute, ); + defer shader.deinit(); + // storage images + self.gpu_state.render_target = try Image.init(self.ctx, .{ + .initial_layout = .general, + .format = .r8g8b8a8_snorm, + .tiling = .linear, + .extent = self.swapchain.extent, + .mem_flags = .{ .device_local_bit = true }, + .usage = .{ + .transfer_dst_bit = true, + .storage_bit = true, + .sampled_bit = true, + }, + .clear_col = .{ .float_32 = .{ 0, 0, 0, 0 } }, + }); + self.gpu_state.render_view = try self.gpu_state.render_target.createView(.{.color_bit = true}); + + self.gpu_state.compute_uniforms = try UniformBuffer(ComputeUniforms).create(self.ctx); + self.gpu_state.particles = try StorageBuffer(Particle).create(self.ctx, PARTICLE_COUNT); const descriptors: []const DescriptorBinding = &.{ .{ @@ -213,7 +254,10 @@ const SampleState = struct { .stages = .{ .compute_bit = true }, }, .{ - .data = .{}, + .data = .{ .Image = .{ + .img = &self.gpu_state.render_target, + .view = self.gpu_state.render_view.h_view, + } }, .stages = .{ .compute_bit = true }, }, }; @@ -221,6 +265,7 @@ const SampleState = struct { .shader = &shader, .desc_bindings = descriptors, }); + } pub fn active(self: *const SampleState) bool { @@ -269,6 +314,7 @@ const SampleState = struct { self.ctx.dev.waitIdle() catch {}; self.graphics.deinit(); + self.compute.deinit(); self.gpu_state.deinit(); self.swapchain.deinit(); @@ -280,6 +326,7 @@ const SampleState = struct { }; pub fn main() !void { + log.debug("LINK START", .{}); const mem = try std.heap.page_allocator.alloc(u8, 1_000_024); defer std.heap.page_allocator.free(mem); @@ -292,6 +339,8 @@ pub fn main() !void { try state.createSyncObjects(); try state.createSwapchain(); try state.createGraphicsPipeline(); + try state.createComputePipelines(); + state.window.show(); while (state.active()) { diff --git a/samples/slime/shaders/compute_slime.glsl b/samples/slime/shaders/compute_slime.glsl index 73672e4..ee0f97e 100644 --- a/samples/slime/shaders/compute_slime.glsl +++ b/samples/slime/shaders/compute_slime.glsl @@ -8,7 +8,8 @@ struct Particle { //TODO: Implement PUSH constants NOW layout(binding = 0) uniform UniformBuffer { vec3 col; - uvec2 res; + uint res_x; + uint res_y; uint particle_count; uint pixels_rad; } uniforms; @@ -18,13 +19,12 @@ layout(std140, binding = 1) buffer ParticlesBuffer{ Particle particles[]; } agents; -// output image //TODO: Needs new descriptor type: Storage image -layout(rgba8_snorm, binding = 2) writeonly image2D output; +layout(rgba8_snorm, binding = 2) uniform writeonly image2D render_target; layout(local_size_x = 8, local_size_y = 8) in; -// just write the particles positions to an output image I guess... +// just write the particles positions to an render_target image I guess... void main() { if (gl_GlobalInvocationID.x > uniforms.particle_count) return; @@ -34,18 +34,18 @@ void main() { int(agents.particles[gl_GlobalInvocationID.x].position.y) ); - vec2 xb = vec2( + ivec2 xb = ivec2( max(0, pos.x - uniforms.pixels_rad), - min(uniforms.res, pos.x + uniforms.pixels_rad) + min(uniforms.res_x, pos.x + uniforms.pixels_rad) ); - vec2 yb = vec2( + ivec2 yb = ivec2( max(0, pos.y - uniforms.pixels_rad), - min(uniforms.res, pos.y + uniforms.pixels_rad) + min(uniforms.res_y, pos.y + uniforms.pixels_rad) ); for (int x = xb.x; x < xb.y; x++) { for (int y = yb.x; y < yb.y; y++) { - imageStore(output, ivec2(x, y), vec4(col, 1.0)); + imageStore(render_target, ivec2(x, y), vec4(uniforms.col, 1.0)); } } } From af499dce345df88ba681e798537ce989e1cbc8f8 Mon Sep 17 00:00:00 2001 From: johnnyboi12 <54293281+JohnSmoit@users.noreply.github.com> Date: Fri, 1 Aug 2025 17:51:12 -0600 Subject: [PATCH 17/22] feat(vulkan): updated layout transitions and switched from EnumMap to switch case for layout access masks --- src/api/base.zig | 14 ++++++ src/api/buffer.zig | 5 ++ src/api/compute.zig | 10 ++-- src/api/descriptor.zig | 32 +++++++++---- src/api/image.zig | 93 ++++++++++++++++++++++---------------- src/api/storage_buffer.zig | 2 +- 6 files changed, 104 insertions(+), 52 deletions(-) diff --git a/src/api/base.zig b/src/api/base.zig index 1c34cba..c084c83 100644 --- a/src/api/base.zig +++ b/src/api/base.zig @@ -393,6 +393,8 @@ pub const DeviceHandler = struct { for (dev_queue_family_props, 0..) |props, index| { const i: u32 = @intCast(index); + log.debug("Family {d}, Supports:\n {s}", .{i, props.queue_flags}); + if (props.queue_flags.contains(.{ .graphics_bit = true, })) { @@ -413,6 +415,18 @@ pub const DeviceHandler = struct { } } + log.debug( + \\The following queue indices support needed + \\families of operations: + \\ {?}: COMPUTE, + \\ {?}: GRAPHICS, + \\ {?}: PRESENT, + , .{ + found_indices.compute_family, + found_indices.graphics_family, + found_indices.present_family, + }); + return found_indices; } diff --git a/src/api/buffer.zig b/src/api/buffer.zig index 387730d..720346d 100644 --- a/src/api/buffer.zig +++ b/src/api/buffer.zig @@ -98,6 +98,7 @@ pub fn DeinitMixin(comptime T: type, val: ?*const (fn(*T) void)) DeinitFn { return Wrapper.f; } +//TODO: Implement AUtoVTable pub fn AutoVTable(comptime T: type) *const VTable { const info = @typeInfo(T); @@ -105,6 +106,10 @@ pub fn AutoVTable(comptime T: type) *const VTable { .Struct => |s| s, else => @compileError("Invalid Vtable base type, must be struct"), }; + + _ = sinfo; + + return undefined; } pub fn copy(src: AnyBuffer, dst: AnyBuffer, dev: *const DeviceHandler) !void { diff --git a/src/api/compute.zig b/src/api/compute.zig index fa3877e..be28348 100644 --- a/src/api/compute.zig +++ b/src/api/compute.zig @@ -27,7 +27,7 @@ desc: Descriptor, pub fn init(ctx: *const Context, allocator: Allocator, cfg: Config) !Self { const pr_dev: *const DeviceInterface = ctx.env(.di); - const desc = try Descriptor.init(ctx, allocator, &.{ + var desc = try Descriptor.init(ctx, allocator, .{ .bindings = cfg.desc_bindings, }); errdefer desc.deinit(); @@ -36,7 +36,7 @@ pub fn init(ctx: *const Context, allocator: Allocator, cfg: Config) !Self { const layout = try pr_dev.createPipelineLayout(&.{ .flags = .{}, .set_layout_count = 1, - .p_set_layouts = &.{&desc.h_desc_layout}, + .p_set_layouts = &.{desc.h_desc_layout}, }, null); var new = Self{ @@ -47,15 +47,15 @@ pub fn init(ctx: *const Context, allocator: Allocator, cfg: Config) !Self { }; const compute_pipeline_info = vk.ComputePipelineCreateInfo{ + .base_pipeline_index = 0, .layout = layout, - .flags = .{}, .stage = cfg.shader.pipeline_info, }; - try pr_dev.createComputePipelines( + _ = try pr_dev.createComputePipelines( .null_handle, 1, - &.{&compute_pipeline_info}, + &.{compute_pipeline_info}, null, @as([*]vk.Pipeline, @ptrCast(&new.h_pipeline)), ); diff --git a/src/api/descriptor.zig b/src/api/descriptor.zig index 8c9c180..440cc15 100644 --- a/src/api/descriptor.zig +++ b/src/api/descriptor.zig @@ -19,6 +19,7 @@ const Context = @import("../context.zig"); const AnyBuffer = buffer.AnyBuffer; const TexImage = @import("texture.zig"); +const Image = @import("image.zig"); const log = std.log.scoped(.descriptor); @@ -38,6 +39,7 @@ pub const DescriptorType = enum(u8) { Uniform, Sampler, StorageBuffer, + Image, }; pub const ResolvedBinding = struct { @@ -46,13 +48,18 @@ pub const ResolvedBinding = struct { Uniform: AnyBuffer, Sampler: *const TexImage, StorageBuffer: AnyBuffer, + Image: struct { + img: *const Image, + // caller is responsible for this for now + view: vk.ImageView, + }, }, }; // flattened since buffers are the same regardless // of whether they be uniform or storage const BindingWriteInfo = union(enum) { - Sampler: vk.DescriptorImageInfo, + Image: vk.DescriptorImageInfo, Buffer: vk.DescriptorBufferInfo, }; @@ -89,6 +96,7 @@ fn resolveDescriptorLayout( .Sampler => .combined_image_sampler, .Uniform => .uniform_buffer, .StorageBuffer => .storage_buffer, + .Image => .storage_image, }, }; } @@ -131,23 +139,31 @@ fn updateDescriptorSets( switch (binding.data) { .Sampler => |tex| { - write_infos[index] = .{ .Sampler = vk.DescriptorImageInfo{ + write_infos[index] = .{ .Image = vk.DescriptorImageInfo{ .image_layout = .read_only_optimal, .image_view = tex.view.h_view, .sampler = tex.h_sampler, } }; writes[index].descriptor_type = .combined_image_sampler; - writes[index].p_image_info = many( - vk.DescriptorImageInfo, - &write_infos[index].Sampler, - ); + writes[index].p_image_info = &.{write_infos[index].Image}; + }, + .Image => |img| { + write_infos[index] = .{ .Image = vk.DescriptorImageInfo{ + .image_layout = .general, + .image_view = img.view, + .sampler = .null_handle, + } }; + + writes[index].p_image_info = &.{write_infos[index].Image}; + writes[index].descriptor_type = .storage_image; + }, else => { const buf: AnyBuffer, const dt: vk.DescriptorType = switch (binding.data) { .Uniform => |buf| .{ buf, .uniform_buffer }, .StorageBuffer => |buf| .{ buf, .storage_buffer }, - .Sampler => unreachable, + else => unreachable, }; write_infos[index] = .{ .Buffer = vk.DescriptorBufferInfo{ @@ -254,7 +270,7 @@ pub fn update(self: *Self, index: usize, data: anytype) !void { switch (binding.data) { .Uniform => |buf| try buf.setData(data), .StorageBuffer => |buf| try buf.setData(data), - .Sampler => { + else => { log.err("Fuck (AKA no texture updating pretty please)", .{}); return error.Unsupported; }, diff --git a/src/api/image.zig b/src/api/image.zig index 07a09c7..bce81d1 100644 --- a/src/api/image.zig +++ b/src/api/image.zig @@ -47,8 +47,7 @@ pub const Config = struct { usage: vk.ImageUsageFlags, format: vk.Format, mem_flags: vk.MemoryPropertyFlags, - width: u32, - height: u32, + extent: vk.Extent2D, tiling: vk.ImageTiling = .linear, clear_col: ?vk.ClearColorValue = null, @@ -113,28 +112,42 @@ pub const AccessMapEntry = struct { access: vk.AccessFlags = .{}, }; -const transition_map = std.enums.EnumMap(vk.ImageLayout, AccessMapEntry).init(.{ - .undefined = .{ - .stage = .{ .top_of_pipe_bit = true }, - .access = .{}, - }, - .general = .{ - .stage = .{ .compute_shader_bit = true }, - .access = .{}, - }, - .transfer_dst_optimal = .{ - .stage = .{ .transfer_bit = true }, - .access = .{ .transfer_write_bit = true }, - }, - .shader_read_only_optimal = .{ - .stage = .{ .fragment_shader_bit = true }, - .access = .{ .shader_read_bit = true }, - }, -}); +//NOTE: Unfortunately, vulkan enums (maybe also c enums in general) +// are too fat to work with the standard library's +// enum map (the compiler runs out of memory trying to allocate a 4-billion bit bitset +// along with a 4-billion entry array of my AccessMapEntry structs) +// probably worth looking into a static hashmap that works off of a predefined range of key-value +// entries and doesn't need allocation + +fn getTransitionParams(layout: vk.ImageLayout) AccessMapEntry { + return switch (layout) { + .undefined => .{ + .stage = .{ .top_of_pipe_bit = true }, + .access = .{}, + }, + .general => .{ + .stage = .{ .compute_shader_bit = true }, + .access = .{}, + }, + .transfer_dst_optimal => .{ + .stage = .{ .transfer_bit = true }, + .access = .{ .transfer_write_bit = true }, + }, + .shader_read_only_optimal => .{ + .stage = .{ .fragment_shader_bit = true }, + .access = .{ .shader_read_bit = true }, + }, + else => extra: { + log.warn("Invalid transition combination specified", .{}); + break :extra .{}; + }, + }; +} pub const LayoutTransitionOptions = struct { - cmd_buf: ?*const CommandBuffer, - access_overrides: ?vk.AccessFlags, + cmd_buf: ?*const CommandBuffer = null, + src_access_overrides: vk.AccessFlags = .{}, + dst_access_overrides: vk.AccessFlags = .{}, }; /// injects a layout transition command into an existing command buffer @@ -182,17 +195,19 @@ pub fn cmdTransitionLayout( // as far as specifying the pipeline stages the barrier sits between, as well as which parts // of the resource should be accessed as the source and destination of the barrier transition - const src_properties = transition_map.get(from) orelse .{}; - const dst_properties = transition_map.get(to) orelse .{}; + const src_properties: AccessMapEntry = getTransitionParams(from); + const dst_properties: AccessMapEntry = getTransitionParams(to); - const src_access = opts.access_overrides orelse .{}; - const dst_access = opts.access_overrides orelse .{}; + const default_src_access: vk.AccessFlags = src_properties.access; + const default_dst_access: vk.AccessFlags = dst_properties.access; - const default_src_access = (transition_map.get(from) orelse .{}).access; - const default_dst_access = (transition_map.get(to) orelse .{}).access; + transition_barrier.src_access_mask = + default_src_access.merge(opts.src_access_overrides); + transition_barrier.dst_access_mask = + default_dst_access.merge(opts.dst_access_overrides); - transition_barrier.src_access_mask = default_src_access.merge(src_access); - transition_barrier.src_access_mask = default_dst_access.merge(dst_access); + log.debug("src access: {s}", .{transition_barrier.src_access_mask}); + log.debug("dst access: {s}", .{transition_barrier.dst_access_mask}); self.dev.pr_dev.cmdPipelineBarrier( cmd_buf.h_cmd_buffer, @@ -204,7 +219,7 @@ pub fn cmdTransitionLayout( 0, null, 1, - &.{&transition_barrier}, + &.{transition_barrier}, ); } @@ -219,8 +234,9 @@ pub fn transitionLayout( defer transition_cmds.deinit(); const opts2 = LayoutTransitionOptions{ - .access_overrides = opts.access_overrides, - .cmd_buf = transition_cmds, + .src_access_overrides = opts.src_access_overrides, + .dst_access_overrides = opts.dst_access_overrides, + .cmd_buf = &transition_cmds, }; self.cmdTransitionLayout(from, to, opts2); @@ -267,10 +283,10 @@ pub fn cmdClear( cur_layout: vk.ImageLayout, ) void { self.dev.pr_dev.cmdClearColorImage( - cmd_buf, + cmd_buf.h_cmd_buffer, self.h_img, cur_layout, - &.{col}, + &col, 1, &.{.{ .aspect_mask = .{ .color_bit = true }, @@ -286,8 +302,8 @@ fn initSelf(self: *Self, dev: *const DeviceHandler, config: Config) !void { const image_info = vk.ImageCreateInfo{ .image_type = .@"2d", .extent = .{ - .width = config.width, - .height = config.height, + .width = config.extent.width, + .height = config.extent.height, .depth = 1, }, .mip_levels = 1, @@ -333,7 +349,7 @@ fn initSelf(self: *Self, dev: *const DeviceHandler, config: Config) !void { // WARN: This disregards the fact that if a staging buffer is not used, then // the user is probably copying from host visible memory (not sure if this handles that // correctly) - const tmp_cmds = try CommandBuffer.oneShot(); + const tmp_cmds = try CommandBuffer.oneShot(self.dev); if (config.clear_col) |col| { self.cmdTransitionLayout(.undefined, .transfer_dst_optimal, .{ @@ -357,6 +373,7 @@ pub fn init(ctx: *const Context, config: Config) !Self { var image = Self{}; try image.initSelf(ctx.env(.dev), config); + log.debug("successfully initiailized image", .{}); return image; } diff --git a/src/api/storage_buffer.zig b/src/api/storage_buffer.zig index bcd253c..2633768 100644 --- a/src/api/storage_buffer.zig +++ b/src/api/storage_buffer.zig @@ -47,7 +47,7 @@ pub fn ComptimeStorageBuffer(comptime T: type) type { cmd_buf.h_cmd_buffer, 0, 1, - &.{&self.buf.h_buf}, + &.{self.buf.h_buf}, &.{0}, ); } From c7a363ba7536cff131697e587154106b49bf4e71 Mon Sep 17 00:00:00 2001 From: johnnyboi12 <54293281+JohnSmoit@users.noreply.github.com> Date: Sun, 3 Aug 2025 18:48:11 -0500 Subject: [PATCH 18/22] feat(vulkan): update various types to handle multiple use images across a pipeline's lifecycle --- src/api/api.zig | 5 ++++ src/api/base.zig | 29 ++++++++++++++------ src/api/buffer.zig | 55 ++++++++++++++++++++++++++++---------- src/api/command_buffer.zig | 46 ++++++++++++++++--------------- src/api/compute.zig | 4 ++- src/api/descriptor.zig | 14 +++++++--- src/api/image.zig | 13 ++++++--- src/api/storage_buffer.zig | 15 +++-------- src/api/types.zig | 9 +++++++ src/api/uniform.zig | 23 ++++++---------- 10 files changed, 136 insertions(+), 77 deletions(-) create mode 100644 src/api/types.zig diff --git a/src/api/api.zig b/src/api/api.zig index 5afab51..4d71e99 100644 --- a/src/api/api.zig +++ b/src/api/api.zig @@ -53,6 +53,10 @@ const BufInterface = buf.AnyBuffer; pub const Descriptor = @import("descriptor.zig"); pub const ResolvedDescriptorBinding = Descriptor.ResolvedBinding; +// additional utility structs and stuff +const types = @import("types.zig"); +pub const SyncInfo = types.SyncInfo; + // sync stuff pub const Semaphore = sync.Semaphore; pub const Fence = sync.Fence; @@ -60,3 +64,4 @@ pub const Fence = sync.Fence; // shaders pub const ShaderModule = sh.Module; + diff --git a/src/api/base.zig b/src/api/base.zig index c084c83..c9088b1 100644 --- a/src/api/base.zig +++ b/src/api/base.zig @@ -339,7 +339,7 @@ pub const DeviceHandler = struct { // HAve the device context manage the command pool // and then all command buffers can be created using the same pool - h_cmd_pool: vk.CommandPool = .null_handle, + h_cmd_pool: [@typeInfo(QueueType).@"enum".fields.len]vk.CommandPool = undefined, props: vk.PhysicalDeviceProperties, pub fn findMemoryTypeIndex( @@ -547,11 +547,7 @@ pub const DeviceHandler = struct { } const dev_proxy = vk.DeviceProxy.init(logical_dev, dev_wrapper); - - const cmd_pool = try dev_proxy.createCommandPool(&.{ - .flags = .{ .reset_command_buffer_bit = true }, - .queue_family_index = dev_queue_indices.graphics_family.?, - }, null); + return DeviceHandler{ .ctx = parent, @@ -559,15 +555,32 @@ pub const DeviceHandler = struct { .pr_dev = dev_proxy, .h_dev = logical_dev, .h_pdev = chosen_dev, - .h_cmd_pool = cmd_pool, + .h_cmd_pool = [3]vk.CommandPool{ + try dev_proxy.createCommandPool(&.{ + .flags = .{ .reset_command_buffer_bit = true }, + .queue_family_index = dev_queue_indices.graphics_family.?, + }, null), + .null_handle, + try dev_proxy.createCommandPool(&.{ + .flags = .{ .reset_command_buffer_bit = true }, + .queue_family_index = dev_queue_indices.compute_family.?, + }, null), + }, .families = dev_queue_indices, .swapchain_details = swapchain_details, .props = dev_properties, }; } + pub fn getCommandPool(self: *const DeviceHandler, fam: api.QueueType) vk.CommandPool { + return self.h_cmd_pool[@intFromEnum(fam)]; + } + pub fn deinit(self: *DeviceHandler) void { - self.pr_dev.destroyCommandPool(self.h_cmd_pool, null); + for (self.h_cmd_pool) |pool| { + if (pool == .null_handle) continue; + self.pr_dev.destroyCommandPool(pool, null); + } self.pr_dev.destroyDevice(null); self.ctx.allocator.destroy(self.dev_wrapper); diff --git a/src/api/buffer.zig b/src/api/buffer.zig index 720346d..d690d61 100644 --- a/src/api/buffer.zig +++ b/src/api/buffer.zig @@ -25,13 +25,28 @@ const SetDataFn = *const (fn (*anyopaque, *const anyopaque) anyerror!void); const DeinitFn = *const (fn (*anyopaque) void); const VTable = struct { - bind: *const (fn (*anyopaque, *const CommandBuffer) void) = undefined, + bind: *const (fn (*anyopaque, *const CommandBuffer) void) = bindNoOp, // this is kinda gross, maybe consider something other than type erasing here... - setData: *const (fn (*anyopaque, *const anyopaque) anyerror!void) = undefined, - deinit: *const (fn (*anyopaque) void) = undefined, + setData: *const (fn (*anyopaque, *const anyopaque) anyerror!void) = setDataNoOp, + deinit: *const (fn (*anyopaque) void) = deinitNoOp, }; +// default functions (NO-Ops) if a corresponding VTable candidate isn't found +pub fn bindNoOp(ctx: *anyopaque, cmd_buf: *const CommandBuffer) void { + _ = ctx; + _ = cmd_buf; +} + +pub fn setDataNoOp(ctx: *anyopaque, data: *const anyopaque) anyerror!void { + _ = ctx; + _ = data; +} + +pub fn deinitNoOp(ctx: *anyopaque) void { + _ = ctx; +} + /// generic buffer interface, exposes common functionality /// for buffer operations such as binding, setting data, and /// lifecycle functions. These are directly meant to be @@ -76,9 +91,9 @@ pub fn BindMixin(comptime T: type, val: ?*const (fn(*T, *const CommandBuffer) vo return Wrapper.f; } -pub fn SetDataMixin(comptime T: type, val: ?*const (fn(*T, *anyopaque) anyerror!void)) SetDataFn { +pub fn SetDataMixin(comptime T: type, val: ?*const (fn(*T, *const anyopaque) anyerror!void)) SetDataFn { const Wrapper = struct { - pub fn f(ctx: *anyopaque, data: *anyopaque) anyerror!void { + pub fn f(ctx: *anyopaque, data: *const anyopaque) anyerror!void { const self: *T = @ptrCast(@alignCast(ctx)); if (val) |func| try func(self, data); } @@ -98,25 +113,35 @@ pub fn DeinitMixin(comptime T: type, val: ?*const (fn(*T) void)) DeinitFn { return Wrapper.f; } + //TODO: Implement AUtoVTable pub fn AutoVTable(comptime T: type) *const VTable { - const info = @typeInfo(T); + const vtable1 = comptime gen_vtable: { + + var vtable = VTable{}; + + if (@hasDecl(T, "bind")) { + vtable.bind = BindMixin(T, @field(T, "bind")); + } + if (@hasDecl(T, "setData")) { + vtable.setData = SetDataMixin(T, @field(T, "setData")); + } + if (@hasDecl(T, "deinit")) { + vtable.deinit = DeinitMixin(T, @field(T, "deinit")); + } - const sinfo = switch(info) { - .Struct => |s| s, - else => @compileError("Invalid Vtable base type, must be struct"), - }; - _ = sinfo; + break :gen_vtable vtable; + }; - return undefined; + return &vtable1; } pub fn copy(src: AnyBuffer, dst: AnyBuffer, dev: *const DeviceHandler) !void { assert(src.cfg.usage.contains(.{ .transfer_src_bit = true })); assert(dst.cfg.usage.contains(.{ .transfer_dst_bit = true })); - const transfer_cmds = try CommandBuffer.oneShot(dev); + const transfer_cmds = try CommandBuffer.oneShot(dev, .{}); defer transfer_cmds.deinit(); dev.pr_dev.cmdCopyBuffer( @@ -132,6 +157,7 @@ pub fn copy(src: AnyBuffer, dst: AnyBuffer, dev: *const DeviceHandler) !void { ); try transfer_cmds.end(); + try transfer_cmds.submit(.Graphics, .{}); } pub fn StagingType(T: type) type { @@ -260,7 +286,7 @@ pub fn GenericBuffer(T: type, comptime config: Config) type { assert(config.usage.contains(.{ .transfer_src_bit = true })); assert(dest.config.usage.contains(.{ .transfer_dst_bit = true })); - const transfer_cmds = try CommandBuffer.oneShot(self.dev); + const transfer_cmds = try CommandBuffer.oneShot(self.dev, .{}); defer transfer_cmds.deinit(); self.dev.pr_dev.cmdCopyBuffer( @@ -276,6 +302,7 @@ pub fn GenericBuffer(T: type, comptime config: Config) type { ); try transfer_cmds.end(); + try transfer_cmds.submit(.Graphics, .{}); } pub fn deinit(self: *const Self) void { diff --git a/src/api/command_buffer.zig b/src/api/command_buffer.zig index e1b911d..b83c4b4 100644 --- a/src/api/command_buffer.zig +++ b/src/api/command_buffer.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const api = @import("api.zig"); const vk = @import("vulkan"); const base = @import("base.zig"); const util = @import("../util.zig"); @@ -17,16 +18,20 @@ h_cmd_pool: vk.CommandPool, dev: *const DeviceHandler, one_shot: bool = false, -pub fn init(ctx: *const Context) !Self { +pub const Config = struct { + src_queue_family: queue.QueueFamily = .Graphics, +}; + +pub fn init(ctx: *const Context, config: Config) !Self { const dev = ctx.env(.dev); - return initDev(dev); + return initDev(dev, config); } -fn initDev(dev: *const DeviceHandler) !Self { +fn initDev(dev: *const DeviceHandler, config: Config) !Self { var cmd_buffer: vk.CommandBuffer = undefined; dev.pr_dev.allocateCommandBuffers( &.{ - .command_pool = dev.h_cmd_pool, + .command_pool = dev.getCommandPool(config.src_queue_family), .level = .primary, .command_buffer_count = 1, }, @@ -38,13 +43,13 @@ fn initDev(dev: *const DeviceHandler) !Self { return .{ .h_cmd_buffer = cmd_buffer, - .h_cmd_pool = dev.h_cmd_pool, + .h_cmd_pool = dev.getCommandPool(config.src_queue_family), .dev = dev, }; } -pub fn oneShot(dev: *const DeviceHandler) !Self { - var buf = try initDev(dev); +pub fn oneShot(dev: *const DeviceHandler, config: Config) !Self { + var buf = try initDev(dev, config); buf.one_shot = true; try buf.beginConfig(.{ .one_time_submit_bit = true }); @@ -70,21 +75,6 @@ pub fn end(self: *const Self) !void { log.err("Command recording failed: {!}", .{err}); return err; }; - - // NOTE: For now, I'm going to just hardcode a submit to the graphics queue - // if a one shot command buffer is used - // Also, synchronization is not gonna be handled yet... - // the best way to handle synchronization is to only do 1 thing at a time 😊 - // (by waiting idle) - - // We need queue handles from the context straight up, no way around it ugh - // this shit is too bad to handle otherwise - if (self.one_shot) { - const submit_queue = self.dev.getQueue(.Graphics) orelse - return error.OneShotSubmitFailed; - try submit_queue.submit(self, null, null, null); - submit_queue.waitIdle(); - } } pub fn reset(self: *const Self) !void { @@ -94,7 +84,19 @@ pub fn reset(self: *const Self) !void { }; } +pub fn submit(self: *const Self, comptime fam: queue.QueueFamily, sync: api.SyncInfo) !void { + const submit_queue = self.dev.getQueue(fam) orelse return error.Unsupported; + try submit_queue.submit( + self, + sync.sem_wait, + sync.sem_sig, + sync.fence_wait, + ); +} + pub fn deinit(self: *const Self) void { + // make sure the command buffer isn't in use before destroying it.. + self.dev.waitIdle() catch {}; self.dev.pr_dev.freeCommandBuffers( self.h_cmd_pool, 1, diff --git a/src/api/compute.zig b/src/api/compute.zig index be28348..ecf53cf 100644 --- a/src/api/compute.zig +++ b/src/api/compute.zig @@ -38,6 +38,7 @@ pub fn init(ctx: *const Context, allocator: Allocator, cfg: Config) !Self { .set_layout_count = 1, .p_set_layouts = &.{desc.h_desc_layout}, }, null); + errdefer pr_dev.destroyPipelineLayout(layout, null); var new = Self{ .pr_dev = pr_dev, @@ -68,7 +69,8 @@ pub fn updateData(self: *Self, binding: u32, data: anytype) !void { } pub fn bind(self: *const Self, cmd_buf: *const CommandBuffer) void { - self.pr_dev.cmdBindPipeline(cmd_buf, .compute, self.h_pipeline); + self.pr_dev.cmdBindPipeline(cmd_buf.h_cmd_buffer, .compute, self.h_pipeline); + self.desc.bind(cmd_buf, self.h_pipeline_layout, .{ .bind_point = .compute }); } pub fn dispatch( diff --git a/src/api/descriptor.zig b/src/api/descriptor.zig index 440cc15..00554be 100644 --- a/src/api/descriptor.zig +++ b/src/api/descriptor.zig @@ -157,7 +157,6 @@ fn updateDescriptorSets( writes[index].p_image_info = &.{write_infos[index].Image}; writes[index].descriptor_type = .storage_image; - }, else => { const buf: AnyBuffer, const dt: vk.DescriptorType = switch (binding.data) { @@ -245,10 +244,19 @@ pub fn init(ctx: *const Context, allocator: Allocator, config: Config) !Self { return descriptor; } -pub fn bind(self: *const Self, cmd_buf: *const CommandBuffer, layout: vk.PipelineLayout) void { +pub const BindInfo = struct { + bind_point: vk.PipelineBindPoint = .graphics, +}; + +pub fn bind( + self: *const Self, + cmd_buf: *const CommandBuffer, + layout: vk.PipelineLayout, + info: BindInfo, +) void { self.pr_dev.cmdBindDescriptorSets( cmd_buf.h_cmd_buffer, - .graphics, + info.bind_point, layout, 0, 1, diff --git a/src/api/image.zig b/src/api/image.zig index bce81d1..2e5e33f 100644 --- a/src/api/image.zig +++ b/src/api/image.zig @@ -230,7 +230,7 @@ pub fn transitionLayout( to: vk.ImageLayout, opts: LayoutTransitionOptions, ) !void { - const transition_cmds = try CommandBuffer.oneShot(self.dev); + const transition_cmds = try CommandBuffer.oneShot(self.dev, .{}); defer transition_cmds.deinit(); const opts2 = LayoutTransitionOptions{ @@ -242,10 +242,12 @@ pub fn transitionLayout( self.cmdTransitionLayout(from, to, opts2); try transition_cmds.end(); + try transition_cmds.submit(.Graphics, .{}); } fn copyFromStaging(self: *Self, staging_buf: *StagingBuffer, extent: vk.Extent3D) !void { - const copy_cmds = try CommandBuffer.oneShot(self.dev); + const copy_cmds = try CommandBuffer.oneShot(self.dev, .{}); + defer copy_cmds.deinit(); self.dev.pr_dev.cmdCopyBufferToImage( copy_cmds.h_cmd_buffer, @@ -274,6 +276,9 @@ fn copyFromStaging(self: *Self, staging_buf: *StagingBuffer, extent: vk.Extent3D log.err("Image copy from staging buffer failed: {!}", .{err}); return err; }; + + try copy_cmds.submit(.Graphics, .{}); + } pub fn cmdClear( @@ -349,7 +354,8 @@ fn initSelf(self: *Self, dev: *const DeviceHandler, config: Config) !void { // WARN: This disregards the fact that if a staging buffer is not used, then // the user is probably copying from host visible memory (not sure if this handles that // correctly) - const tmp_cmds = try CommandBuffer.oneShot(self.dev); + const tmp_cmds = try CommandBuffer.oneShot(self.dev, .{}); + defer tmp_cmds.deinit(); if (config.clear_col) |col| { self.cmdTransitionLayout(.undefined, .transfer_dst_optimal, .{ @@ -366,6 +372,7 @@ fn initSelf(self: *Self, dev: *const DeviceHandler, config: Config) !void { } try tmp_cmds.end(); + try tmp_cmds.submit(.Graphics, .{}); } } diff --git a/src/api/storage_buffer.zig b/src/api/storage_buffer.zig index 2633768..f55619b 100644 --- a/src/api/storage_buffer.zig +++ b/src/api/storage_buffer.zig @@ -26,8 +26,7 @@ pub fn ComptimeStorageBuffer(comptime T: type) type { }; } - pub fn setData(ctx: *anyopaque, data: *const anyopaque) !void { - const self: *Self = @ptrCast(@alignCast(ctx)); + pub fn setData(self: *Self, data: *const anyopaque) !void { const elem: []const T = @as([*]const T, @ptrCast(@alignCast(data)))[0..self.buf.size]; var staging = try self.buf.createStaging(); @@ -41,8 +40,7 @@ pub fn ComptimeStorageBuffer(comptime T: type) type { try buf_api.copy(staging.buffer(), self.buffer(), self.buf.dev); } - pub fn bind(ctx: *anyopaque, cmd_buf: *const CommandBuffer) void { - const self: *Self = @ptrCast(@alignCast(ctx)); + pub fn bind(self: *Self, cmd_buf: *const CommandBuffer) void { self.buf.dev.pr_dev.cmdBindVertexBuffers( cmd_buf.h_cmd_buffer, 0, @@ -51,8 +49,7 @@ pub fn ComptimeStorageBuffer(comptime T: type) type { &.{0}, ); } - pub fn deinit(ctx: *anyopaque) void { - const self: *Self = @ptrCast(@alignCast(ctx)); + pub fn deinit(self: *Self) void { self.buf.deinit(); } @@ -62,11 +59,7 @@ pub fn ComptimeStorageBuffer(comptime T: type) type { .handle = self.buf.h_buf, .ptr = self, .size = self.buf.bytesSize(), - .vtable = &.{ - .bind = bind, - .setData = setData, - .deinit = deinit, - }, + .vtable = buf_api.AutoVTable(Self), }; } }; diff --git a/src/api/types.zig b/src/api/types.zig new file mode 100644 index 0000000..4f40141 --- /dev/null +++ b/src/api/types.zig @@ -0,0 +1,9 @@ +const vk = @import("vulkan"); + +pub const SyncInfo = struct { + fence_sig: ?vk.Fence = null, + fence_wait: ?vk.Fence = null, + + sem_sig: ?vk.Semaphore = null, + sem_wait: ?vk.Semaphore = null, +}; diff --git a/src/api/uniform.zig b/src/api/uniform.zig index 9b442a9..3ed0e94 100644 --- a/src/api/uniform.zig +++ b/src/api/uniform.zig @@ -1,13 +1,13 @@ //! Type for uniform buffers -const buffer = @import("buffer.zig"); +const buf_api = @import("buffer.zig"); const DeviceHandler = @import("base.zig").DeviceHandler; const CommandBuffer = @import("command_buffer.zig"); const Context = @import("../context.zig"); -const AnyBuffer = buffer.AnyBuffer; -const GenericBuffer = buffer.GenericBuffer; +const AnyBuffer = buf_api.AnyBuffer; +const GenericBuffer = buf_api.GenericBuffer; pub fn UniformBuffer(T: type) type { return struct { @@ -34,10 +34,10 @@ pub fn UniformBuffer(T: type) type { return Self{ .mem = mem, .buf = buf }; } - pub fn bind(ctx: *anyopaque, cmd_buf: *const CommandBuffer) void { + pub fn bind(self: *Self, cmd_buf: *const CommandBuffer) void { // No-Op - _ = ctx; + _ = self; _ = cmd_buf; } @@ -47,24 +47,17 @@ pub fn UniformBuffer(T: type) type { .handle = self.buf.h_buf, .ptr = self, .size = self.buf.bytesSize(), - .vtable = &.{ - .bind = bind, - .setData = setData, - .deinit = deinit, - }, + .vtable = buf_api.AutoVTable(Self), }; } - pub fn setData(ctx: *anyopaque, data: *const anyopaque) !void { - const self: *Self = @ptrCast(@alignCast(ctx)); + pub fn setData(self: *Self, data: *const anyopaque) !void { const elem: []const T = @as([*]const T, @ptrCast(@alignCast(data)))[0..self.buf.size]; @memcpy(self.mem, elem); } - pub fn deinit(ctx: *anyopaque) void { - const self: *Self = @ptrCast(@alignCast(ctx)); - + pub fn deinit(self: *Self) void { self.buf.unmapMemory(); self.buf.deinit(); } From 32360e705d981b9ac1991e534e9e2cfa4fcf8469 Mon Sep 17 00:00:00 2001 From: johnnyboi12 <54293281+JohnSmoit@users.noreply.github.com> Date: Sun, 3 Aug 2025 18:48:40 -0500 Subject: [PATCH 19/22] feat(sample): added a working compute shader dispatch test cycle --- samples/common/render_quad.zig | 2 +- samples/slime/main.zig | 71 ++++++++++++++++++++++++++++++---- src/context.zig | 12 +----- 3 files changed, 66 insertions(+), 19 deletions(-) diff --git a/samples/common/render_quad.zig b/samples/common/render_quad.zig index 7233b9f..787753b 100644 --- a/samples/common/render_quad.zig +++ b/samples/common/render_quad.zig @@ -119,7 +119,7 @@ pub fn drawOneShot(self: *const Self, cmd_buf: *const CommandBuffer, framebuffer self.renderpass.begin(cmd_buf, framebuffer, image_index); if (self.desc) |d| { - d.bind(cmd_buf, self.pipeline.h_pipeline_layout); + d.bind(cmd_buf, self.pipeline.h_pipeline_layout, .{}); } self.dev.draw(cmd_buf, 6, 1, 0, 0); diff --git a/samples/slime/main.zig b/samples/slime/main.zig index b8553d5..5c7a1a4 100644 --- a/samples/slime/main.zig +++ b/samples/slime/main.zig @@ -110,9 +110,9 @@ const GPUState = struct { particles: StorageBuffer(Particle), pub fn deinit(self: *GPUState) void { - self.particles.buffer().deinit(); - self.uniforms.buffer().deinit(); - self.compute_uniforms.buffer().deinit(); + self.particles.deinit(); + self.uniforms.deinit(); + self.compute_uniforms.deinit(); self.render_view.deinit(); self.render_target.deinit(); @@ -182,7 +182,7 @@ const SampleState = struct { self.gpu_state.uniforms = try UniformBuffer(ApplicationUniforms) .create(self.ctx); - try self.gpu_state.uniforms.buffer().setData(&self.gpu_state.host_uniforms); + try self.gpu_state.uniforms.setData(&self.gpu_state.host_uniforms); // create fragment-specific descriptors self.graphics.descriptor = try Descriptor.init(self.ctx, self.allocator, .{ @@ -204,7 +204,7 @@ const SampleState = struct { .renderpass = &self.graphics.render_quad.renderpass, }); - self.graphics.cmd_buf = try CommandBuffer.init(self.ctx); + self.graphics.cmd_buf = try CommandBuffer.init(self.ctx, .{}); } pub fn createSyncObjects(self: *SampleState) !void { @@ -216,7 +216,7 @@ const SampleState = struct { } pub fn createComputePipelines(self: *SampleState) !void { - self.compute.cmd_buf = try CommandBuffer.init(self.ctx); + self.compute.cmd_buf = try CommandBuffer.init(self.ctx, .{ .src_queue_family = .Compute }); const shader = try helpers.initSampleShader( self.ctx, @@ -239,7 +239,7 @@ const SampleState = struct { }, .clear_col = .{ .float_32 = .{ 0, 0, 0, 0 } }, }); - self.gpu_state.render_view = try self.gpu_state.render_target.createView(.{.color_bit = true}); + self.gpu_state.render_view = try self.gpu_state.render_target.createView(.{ .color_bit = true }); self.gpu_state.compute_uniforms = try UniformBuffer(ComputeUniforms).create(self.ctx); self.gpu_state.particles = try StorageBuffer(Particle).create(self.ctx, PARTICLE_COUNT); @@ -265,7 +265,58 @@ const SampleState = struct { .shader = &shader, .desc_bindings = descriptors, }); + } + + pub fn initComputeData(self: *SampleState) !void { + // initialize compute shader uniforms + try self.gpu_state.compute_uniforms.setData(&ComputeUniforms{ + .col = math.vec(.{ 1.0, 1.0, 0.0 }), + .particle_count = PARTICLE_COUNT, + .pixels_rad = 20, + .res_x = self.swapchain.extent.width, + .res_y = self.swapchain.extent.height, + }); + // randomize particle starting positions + const seed: u64 = @intCast(@max(0, std.time.timestamp())); + var rand_engine = std.Random.Sfc64.init(seed); + const rand_generator = std.Random.init( + &rand_engine, + std.Random.Sfc64.fill, + ); + + const particles_temp = try self.allocator.alloc(Particle, PARTICLE_COUNT); + + const res_xf: f32 = @floatFromInt(self.swapchain.extent.width); + const res_yf: f32 = @floatFromInt(self.swapchain.extent.height); + for (particles_temp) |*particle| { + particle.* = Particle{ + .position = math.vec(.{ + rand_generator.float(f32) * res_xf, + rand_generator.float(f32) * res_yf, + 0, + 0, + }), + }; + } + + try self.gpu_state.particles.setData(particles_temp.ptr); + log.debug("Successfully initialized compute shader data", .{}); + } + + pub fn testCompute(self: *SampleState) !void { + const tmp_cmds = try CommandBuffer.oneShot(self.ctx.env(.dev), .{ + .src_queue_family = .Compute, + }); + self.compute.slime_pipeline.bind(&tmp_cmds); + self.compute.slime_pipeline.dispatch(&tmp_cmds, 4, 4, 1); + try tmp_cmds.end(); + try tmp_cmds.submit(.Compute, .{}); + try self.ctx.dev.waitIdle(); + + // transition render layout to shader sampler friendly + try self.gpu_state.render_target.transitionLayout(.general, .shader_read_only_optimal, .{}); + try self.ctx.dev.waitIdle(); } pub fn active(self: *const SampleState) bool { @@ -274,7 +325,7 @@ const SampleState = struct { fn updateUniforms(self: *SampleState) !void { self.gpu_state.host_uniforms.time = @floatCast(glfw.getTime()); - try self.gpu_state.uniforms.buffer().setData(&self.gpu_state.host_uniforms); + try self.gpu_state.uniforms.setData(&self.gpu_state.host_uniforms); } // intercepts errors and logs them @@ -340,9 +391,13 @@ pub fn main() !void { try state.createSwapchain(); try state.createGraphicsPipeline(); try state.createComputePipelines(); + try state.initComputeData(); state.window.show(); + // test the compyuter shader + try state.testCompute(); + while (state.active()) { state.update() catch |err| { log.err("An error occured while running: {!}\n ....Terminating", .{err}); diff --git a/src/context.zig b/src/context.zig index 10f088c..5815999 100644 --- a/src/context.zig +++ b/src/context.zig @@ -189,19 +189,11 @@ pub fn deinit(self: *Self) void { alloc.destroy(self); } -pub const SyncInfo = struct { - fence_sig: ?vk.Fence = null, - fence_wait: ?vk.Fence = null, - - sem_sig: ?vk.Semaphore = null, - sem_wait: ?vk.Semaphore = null, -}; - pub fn submitCommands( self: *Self, cmd_buf: *const CommandBuffer, comptime queue_family: QueueType, - sync: SyncInfo, + sync: api.SyncInfo, ) !void { const queue = switch (queue_family) { .Graphics => self.graphics_queue, @@ -220,7 +212,7 @@ pub fn submitCommands( pub fn presentFrame( self: *Self, swapchain: *const Swapchain, - sync: SyncInfo, + sync: api.SyncInfo, ) !void { const image = swapchain.image_index; try self.present_queue.present(swapchain, image, sync.sem_wait); From 41101126fd15f4a0ca18a6e6a216eecaceb47944 Mon Sep 17 00:00:00 2001 From: johnnyboi12 <54293281+JohnSmoit@users.noreply.github.com> Date: Wed, 6 Aug 2025 10:51:41 -0700 Subject: [PATCH 20/22] feat(sample): compute-generated image now dispays on the window surface --- samples/slime/main.zig | 15 +++++++++++---- samples/slime/shaders/frag.glsl | 13 ++++++++----- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/samples/slime/main.zig b/samples/slime/main.zig index 5c7a1a4..3ed2f54 100644 --- a/samples/slime/main.zig +++ b/samples/slime/main.zig @@ -100,6 +100,7 @@ const GPUState = struct { // render_target: Image, render_view: Image.View, + render_sampler: vk.Sampler, // compute visible only // (contains source image data to base simulation off of) @@ -186,10 +187,16 @@ const SampleState = struct { // create fragment-specific descriptors self.graphics.descriptor = try Descriptor.init(self.ctx, self.allocator, .{ - .bindings = &.{.{ + .bindings = &.{ .{ .data = .{ .Uniform = self.gpu_state.uniforms.buffer() }, .stages = .{ .fragment_bit = true }, - }}, + }, DescriptorBinding{ + .data = .{ .Sampler = .{ + .sampler = self.gpu_state.render_sampler, + .view = self.gpu_state.render_view.h_view, + } }, + .stages = .{ .fragment_bit = true }, + } }, }); try self.graphics.render_quad.initSelf(self.ctx, self.allocator, .{ @@ -240,6 +247,7 @@ const SampleState = struct { .clear_col = .{ .float_32 = .{ 0, 0, 0, 0 } }, }); self.gpu_state.render_view = try self.gpu_state.render_target.createView(.{ .color_bit = true }); + self.gpu_state.render_sampler = try self.gpu_state.render_target.getSampler(.{}); self.gpu_state.compute_uniforms = try UniformBuffer(ComputeUniforms).create(self.ctx); self.gpu_state.particles = try StorageBuffer(Particle).create(self.ctx, PARTICLE_COUNT); @@ -348,7 +356,6 @@ const SampleState = struct { ); try self.graphics.cmd_buf.end(); - // submit the command buffer to a synchronized queue try self.ctx.submitCommands(&self.graphics.cmd_buf, .Graphics, .{ .fence_wait = self.sync.frame_fence.h_fence, @@ -389,8 +396,8 @@ pub fn main() !void { try state.createContext(); try state.createSyncObjects(); try state.createSwapchain(); - try state.createGraphicsPipeline(); try state.createComputePipelines(); + try state.createGraphicsPipeline(); try state.initComputeData(); state.window.show(); diff --git a/samples/slime/shaders/frag.glsl b/samples/slime/shaders/frag.glsl index cc7c518..3d8701a 100644 --- a/samples/slime/shaders/frag.glsl +++ b/samples/slime/shaders/frag.glsl @@ -8,10 +8,13 @@ layout(binding = 0) uniform SampleUniforms { vec2 mouse; } uniforms; +layout(binding = 1) uniform sampler2D compute_image; + void main() { - vec2 ot = vec2( - min(1.0, texCoord.x + sin(uniforms.time)), - min(1.0, texCoord.y + cos(uniforms.time)) - ); - fragColor = vec4(ot, 0.0, 1.0); + // vec2 ot = vec2( + // min(1.0, texCoord.x + sin(uniforms.time)), + // min(1.0, texCoord.y + cos(uniforms.time)) + // ); + + fragColor = texture(compute_image, texCoord); } From d04454279dba39e6d63592eda720a4f7b421960e Mon Sep 17 00:00:00 2001 From: johnnyboi12 <54293281+JohnSmoit@users.noreply.github.com> Date: Wed, 6 Aug 2025 10:52:23 -0700 Subject: [PATCH 21/22] feat(vulkan): updated sampler descriptor to take actual vk.Sampler handle for more direct control --- src/api/descriptor.zig | 11 +++++--- src/api/image.zig | 62 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 63 insertions(+), 10 deletions(-) diff --git a/src/api/descriptor.zig b/src/api/descriptor.zig index 00554be..a449b53 100644 --- a/src/api/descriptor.zig +++ b/src/api/descriptor.zig @@ -46,7 +46,10 @@ pub const ResolvedBinding = struct { stages: vk.ShaderStageFlags, data: union(DescriptorType) { Uniform: AnyBuffer, - Sampler: *const TexImage, + Sampler: struct { + sampler: vk.Sampler, + view: vk.ImageView, + }, StorageBuffer: AnyBuffer, Image: struct { img: *const Image, @@ -138,11 +141,11 @@ fn updateDescriptorSets( self.resolved_bindings[index] = binding; switch (binding.data) { - .Sampler => |tex| { + .Sampler => |sampler| { write_infos[index] = .{ .Image = vk.DescriptorImageInfo{ .image_layout = .read_only_optimal, - .image_view = tex.view.h_view, - .sampler = tex.h_sampler, + .image_view = sampler.view, + .sampler = sampler.sampler, } }; writes[index].descriptor_type = .combined_image_sampler; diff --git a/src/api/image.zig b/src/api/image.zig index 2e5e33f..cd245cd 100644 --- a/src/api/image.zig +++ b/src/api/image.zig @@ -38,6 +38,7 @@ pub const View = struct { h_img: vk.Image = .null_handle, h_mem: vk.DeviceMemory = .null_handle, +h_sampler: ?vk.Sampler = null, dev: *const DeviceHandler = undefined, @@ -81,6 +82,52 @@ fn createImageMemory( return mem; } +pub const SamplerConfig = struct { + mag_filter: vk.Filter = .linear, + min_filter: vk.Filter = .linear, + address_mode: vk.SamplerAddressMode = .repeat, +}; + +/// ## Notes +/// * lazily initializes the sampler if it doesnt exist +/// * the image owns the sampler so no need to destroy it manually +pub fn getSampler(self: *Self, config: SamplerConfig) !vk.Sampler { + if (self.h_sampler) |s| return s; + const max_aniso = self.dev.props.limits.max_sampler_anisotropy; + + self.h_sampler = try self.dev.pr_dev.createSampler(&.{ + .mag_filter = config.mag_filter, + .min_filter = config.min_filter, + + .address_mode_u = config.address_mode, + .address_mode_v = config.address_mode, + .address_mode_w = config.address_mode, + + .anisotropy_enable = vk.TRUE, + .max_anisotropy = max_aniso, + + // If address mode is set to border clamping, this is the color the sampler + // will return if sampled beyond the image's limits + .border_color = .int_opaque_black, + + // As a shader freak, I generally prefer my sampler coordinates to be normalized :} + .unnormalized_coordinates = vk.FALSE, + + // Which comparison option to use when sampler filtering occurs + // (sometimes helpful for shadow mapping apparently) + .compare_enable = vk.FALSE, + .compare_op = .always, + + // Mipmapping stuff -- TBD + .mipmap_mode = .linear, + .mip_lod_bias = 0, + .min_lod = 0, + .max_lod = 0, + }, null); + + return self.h_sampler.?; +} + pub fn createView(self: *const Self, aspect_mask: vk.ImageAspectFlags) !View { const view = try self.dev.pr_dev.createImageView(&.{ .image = self.h_img, @@ -206,8 +253,8 @@ pub fn cmdTransitionLayout( transition_barrier.dst_access_mask = default_dst_access.merge(opts.dst_access_overrides); - log.debug("src access: {s}", .{transition_barrier.src_access_mask}); - log.debug("dst access: {s}", .{transition_barrier.dst_access_mask}); + log.debug("src access: {s}", .{transition_barrier.src_access_mask}); + log.debug("dst access: {s}", .{transition_barrier.dst_access_mask}); self.dev.pr_dev.cmdPipelineBarrier( cmd_buf.h_cmd_buffer, @@ -278,7 +325,6 @@ fn copyFromStaging(self: *Self, staging_buf: *StagingBuffer, extent: vk.Extent3D }; try copy_cmds.submit(.Graphics, .{}); - } pub fn cmdClear( @@ -384,9 +430,9 @@ pub fn init(ctx: *const Context, config: Config) !Self { return image; } -/// creates a texture image and loads it from a provided file -/// WARN: This is a shit way of differentiating images between textures and other image types -/// Actually, this is shit in general, like this shit should be in the texture.zig like wtf +/// creates an image from an image file. +/// Image parameters will be tuned for usage as a texture +/// more so than a general purpose image for now... pub fn fromFile(ctx: *const Context, allocator: Allocator, path: []const u8) !Self { var image_data = rsh.loadImageFile(path, allocator) catch |err| { log.err("Failed to load image: {!}", .{err}); @@ -417,6 +463,10 @@ pub fn fromFile(ctx: *const Context, allocator: Allocator, path: []const u8) !Se } pub fn deinit(self: *const Self) void { + if (self.h_sampler) |s| { + self.dev.pr_dev.destroySampler(s, null); + } + self.dev.pr_dev.destroyImage(self.h_img, null); self.dev.pr_dev.freeMemory(self.h_mem, null); } From ee97baa88325bbefe2db5828eef735c9c5bb2f05 Mon Sep 17 00:00:00 2001 From: johnnyboi12 <54293281+JohnSmoit@users.noreply.github.com> Date: Wed, 6 Aug 2025 11:03:51 -0700 Subject: [PATCH 22/22] refactor 'slime' sample to 'basic compute drawing' --- build.zig | 2 +- samples/{slime => compute_drawing}/main.zig | 33 +++++++------------ .../shaders/compute_map.glsl | 0 .../shaders/compute_slime.glsl | 0 .../shaders/frag.glsl | 0 5 files changed, 13 insertions(+), 22 deletions(-) rename samples/{slime => compute_drawing}/main.zig (93%) rename samples/{slime => compute_drawing}/shaders/compute_map.glsl (100%) rename samples/{slime => compute_drawing}/shaders/compute_slime.glsl (100%) rename samples/{slime => compute_drawing}/shaders/frag.glsl (100%) diff --git a/build.zig b/build.zig index 8a279a2..db0955a 100644 --- a/build.zig +++ b/build.zig @@ -93,7 +93,7 @@ const SampleEntry = struct { }; var sample_files = [_]SampleEntry{ .{ .name = "basic_planes", .path = "basic_planes.zig" }, - .{ .name = "slime", .path = "slime/main.zig" }, + .{ .name = "compute_drawing", .path = "compute_drawing/main.zig" }, .{ .name = "test_sample", .path = "test_sample.zig" }, }; diff --git a/samples/slime/main.zig b/samples/compute_drawing/main.zig similarity index 93% rename from samples/slime/main.zig rename to samples/compute_drawing/main.zig index 3ed2f54..0c9cab1 100644 --- a/samples/slime/main.zig +++ b/samples/compute_drawing/main.zig @@ -1,3 +1,7 @@ +//! Basic example of compute shader initialization using the Low-Level +//! unmanaged Vulkan API. +//! NOTE: This example will likely be deprecated in favor of using the upcoming +//! Managed low level API (see milestone 0.1). const std = @import("std"); const ray = @import("ray"); const glfw = @import("glfw"); @@ -38,15 +42,11 @@ const Compute = api.Compute; const log = std.log.scoped(.application); const ComputeState = struct { - // pipeline for running slime simulation - slime_pipeline: Compute, - // pipeline for updating pheremone map - stinky_pipeline: Compute, - + pipeline: Compute, cmd_buf: CommandBuffer, pub fn deinit(self: *ComputeState) void { - self.slime_pipeline.deinit(); + self.pipeline.deinit(); self.cmd_buf.deinit(); } }; @@ -94,20 +94,11 @@ const GPUState = struct { // compute and graphics visible (needs viewport quad) // written to in the compute shader and simply mapped to the viewport - // with a few transformations for color and such. - // ... This also represents the pheremone map that sim agents in the compute - // shader would use to determine their movements - // render_target: Image, render_view: Image.View, render_sampler: vk.Sampler, // compute visible only - // (contains source image data to base simulation off of) - src_image: Image, - - // compute visible only - // -- contains simulation agents particles: StorageBuffer(Particle), pub fn deinit(self: *GPUState) void { @@ -145,7 +136,7 @@ const SampleState = struct { sync: SyncState = undefined, pub fn createContext(self: *SampleState) !void { - self.window = try helpers.makeBasicWindow(900, 600, "BAD APPLE >:}"); + self.window = try helpers.makeBasicWindow(900, 600, "Drawing with Compute"); self.ctx = try Context.init(self.allocator, .{ .inst_extensions = helpers.glfwInstanceExtensions(), .loader = glfw.glfwGetInstanceProcAddress, @@ -168,7 +159,7 @@ const SampleState = struct { const frag_shader = try helpers.initSampleShader( self.ctx, self.allocator, - "slime/shaders/frag.glsl", + "compute_drawing/shaders/frag.glsl", .Fragment, ); @@ -228,7 +219,7 @@ const SampleState = struct { const shader = try helpers.initSampleShader( self.ctx, self.allocator, - "slime/shaders/compute_slime.glsl", + "compute_drawing/shaders/compute_slime.glsl", .Compute, ); defer shader.deinit(); @@ -269,7 +260,7 @@ const SampleState = struct { .stages = .{ .compute_bit = true }, }, }; - self.compute.slime_pipeline = try Compute.init(self.ctx, self.allocator, .{ + self.compute.pipeline = try Compute.init(self.ctx, self.allocator, .{ .shader = &shader, .desc_bindings = descriptors, }); @@ -316,8 +307,8 @@ const SampleState = struct { const tmp_cmds = try CommandBuffer.oneShot(self.ctx.env(.dev), .{ .src_queue_family = .Compute, }); - self.compute.slime_pipeline.bind(&tmp_cmds); - self.compute.slime_pipeline.dispatch(&tmp_cmds, 4, 4, 1); + self.compute.pipeline.bind(&tmp_cmds); + self.compute.pipeline.dispatch(&tmp_cmds, 4, 4, 1); try tmp_cmds.end(); try tmp_cmds.submit(.Compute, .{}); try self.ctx.dev.waitIdle(); diff --git a/samples/slime/shaders/compute_map.glsl b/samples/compute_drawing/shaders/compute_map.glsl similarity index 100% rename from samples/slime/shaders/compute_map.glsl rename to samples/compute_drawing/shaders/compute_map.glsl diff --git a/samples/slime/shaders/compute_slime.glsl b/samples/compute_drawing/shaders/compute_slime.glsl similarity index 100% rename from samples/slime/shaders/compute_slime.glsl rename to samples/compute_drawing/shaders/compute_slime.glsl diff --git a/samples/slime/shaders/frag.glsl b/samples/compute_drawing/shaders/frag.glsl similarity index 100% rename from samples/slime/shaders/frag.glsl rename to samples/compute_drawing/shaders/frag.glsl