Put Vulkan in your GTK with VkArea

all hail the holy triangle

pluie.me 63ec93ef 427c5c3d

verified
+287 -7
+23
build.zig
··· 37 37 exe_module.linkSystemLibrary("vulkan", .{}); 38 38 } 39 39 40 + { 41 + // Shader 42 + const shader = b.addSystemCommand(&.{ 43 + "slangc", 44 + "-target", 45 + "spirv", 46 + "-profile", 47 + "spirv_1_4", 48 + "-emit-spirv-directly", 49 + "-fvk-use-entrypoint-name", 50 + "-entry", 51 + "vertMain", 52 + "-entry", 53 + "fragMain", 54 + }); 55 + shader.addFileArg(b.path("src/triangle.slang")); 56 + shader.addArg("-o"); 57 + const output = shader.addOutputFileArg("triangle.spv"); 58 + exe_module.addAnonymousImport("triangle_spv", .{ 59 + .root_source_file = output, 60 + }); 61 + } 62 + 40 63 const exe = b.addExecutable(.{ 41 64 .name = "gtk-vk-test", 42 65 .root_module = exe_module,
+1
flake.nix
··· 17 17 vulkan-tools 18 18 vulkan-tools-lunarg 19 19 renderdoc 20 + shader-slang 20 21 ]; 21 22 22 23 env.VULKAN_SDK = "${pkgs.vulkan-headers}";
+160
src/main.zig
··· 8 8 const vk_area = @import("vk_area.zig"); 9 9 const VkArea = vk_area.VkArea; 10 10 11 + const triangle_spv = @embedFile("triangle_spv"); 12 + 11 13 const Application = extern struct { 12 14 parent: Parent, 13 15 pub const Parent = gtk.Application; ··· 81 83 82 84 fn draw(self: *Window, ctx: *const vk_area.RenderContext) !void { 83 85 _ = self; 86 + 87 + // Setup shaders 88 + 89 + const shader = try ctx.device.createShaderModule(&.{ 90 + .code_size = triangle_spv.len, 91 + .p_code = @ptrCast(@alignCast(triangle_spv)), 92 + }, null); 93 + const shader_stages = [_]vk.PipelineShaderStageCreateInfo{ 94 + .{ 95 + .module = shader, 96 + .stage = .{ .vertex_bit = true }, 97 + .p_name = "vertMain", 98 + }, 99 + .{ 100 + .module = shader, 101 + .stage = .{ .fragment_bit = true }, 102 + .p_name = "fragMain", 103 + }, 104 + }; 105 + 106 + // Setup pipeline layout 107 + const pipeline_layout = try ctx.device.createPipelineLayout( 108 + &.{}, 109 + null, 110 + ); 111 + // defer ctx.device.destroyPipelineLayout( 112 + // pipeline_layout, 113 + // null, 114 + // ); 115 + 116 + // Setup pipeline 117 + const pipeline_rendering_info: vk.PipelineRenderingCreateInfo = .{ 118 + .color_attachment_count = 1, 119 + .p_color_attachment_formats = &.{vk_area.format}, 120 + .view_mask = 0, 121 + .depth_attachment_format = .undefined, 122 + .stencil_attachment_format = .undefined, 123 + }; 124 + const dynamic_states = [_]vk.DynamicState{ 125 + .viewport, 126 + .scissor, 127 + }; 128 + const attachment_states = [_]vk.PipelineColorBlendAttachmentState{.{ 129 + .blend_enable = .true, 130 + 131 + .src_color_blend_factor = .src_alpha, 132 + .dst_color_blend_factor = .one_minus_src_alpha, 133 + .color_blend_op = .add, 134 + .src_alpha_blend_factor = .one, 135 + .dst_alpha_blend_factor = .zero, 136 + .alpha_blend_op = .add, 137 + .color_write_mask = .{ 138 + .r_bit = true, 139 + .g_bit = true, 140 + .b_bit = true, 141 + .a_bit = true, 142 + }, 143 + }}; 144 + 145 + const pipeline_info: vk.GraphicsPipelineCreateInfo = .{ 146 + .layout = pipeline_layout, 147 + .stage_count = shader_stages.len, 148 + .p_stages = &shader_stages, 149 + .p_dynamic_state = &.{ 150 + .dynamic_state_count = dynamic_states.len, 151 + .p_dynamic_states = &dynamic_states, 152 + }, 153 + .p_color_blend_state = &.{ 154 + .logic_op_enable = .false, 155 + .logic_op = .copy, 156 + .blend_constants = @splat(0), 157 + .attachment_count = attachment_states.len, 158 + .p_attachments = &attachment_states, 159 + }, 160 + .p_input_assembly_state = &.{ 161 + .topology = .triangle_list, 162 + .primitive_restart_enable = .false, 163 + }, 164 + .p_multisample_state = &.{ 165 + .rasterization_samples = .{ 166 + .@"1_bit" = true, 167 + }, 168 + .sample_shading_enable = .false, 169 + .min_sample_shading = 0, 170 + .alpha_to_coverage_enable = .false, 171 + .alpha_to_one_enable = .false, 172 + }, 173 + .p_rasterization_state = &.{ 174 + .depth_clamp_enable = .false, 175 + .rasterizer_discard_enable = .false, 176 + .polygon_mode = .fill, 177 + .cull_mode = .{ .back_bit = true }, 178 + .front_face = .clockwise, 179 + .depth_bias_enable = .false, 180 + .depth_bias_constant_factor = 0, 181 + .depth_bias_clamp = 0, 182 + .depth_bias_slope_factor = 0, 183 + .line_width = 1, 184 + }, 185 + .p_viewport_state = &.{ 186 + .viewport_count = 1, 187 + .scissor_count = 1, 188 + }, 189 + .p_vertex_input_state = &.{}, 190 + .subpass = 0, 191 + .render_pass = .null_handle, 192 + .base_pipeline_index = -1, 193 + .p_next = @ptrCast(&pipeline_rendering_info), 194 + }; 195 + var pipelines: [1]vk.Pipeline = undefined; 196 + 197 + switch (try ctx.device.createGraphicsPipelines( 198 + .null_handle, 199 + 1, 200 + &.{pipeline_info}, 201 + null, 202 + &pipelines, 203 + )) { 204 + .success => {}, 205 + else => return error.FailedToGraphicsPipeline, 206 + } 207 + // defer for (pipelines) |p| ctx.device.destroyPipeline(p, null); 208 + 84 209 var color = [4]f32{ 0.824, 0.216, 0.451, 1.0 }; 85 210 for (&color) |*ch| ch.* = srgb2Linear(ch.*); 86 211 ··· 107 232 .p_color_attachments = &color_attachments, 108 233 }); 109 234 defer ctx.device.cmdEndRendering(ctx.cmds); 235 + 236 + ctx.device.cmdBindPipeline( 237 + ctx.cmds, 238 + .graphics, 239 + pipelines[0], 240 + ); 241 + ctx.device.cmdSetViewport( 242 + ctx.cmds, 243 + 0, 244 + 1, 245 + &.{.{ 246 + .x = 0, 247 + .y = 0, 248 + .width = @floatFromInt(ctx.extent.width / 2), 249 + .height = @floatFromInt(ctx.extent.height / 2), 250 + .min_depth = 0, 251 + .max_depth = 1, 252 + }}, 253 + ); 254 + ctx.device.cmdSetScissor( 255 + ctx.cmds, 256 + 0, 257 + 1, 258 + &.{.{ 259 + .offset = .{ .x = 0, .y = 0 }, 260 + .extent = ctx.extent, 261 + }}, 262 + ); 263 + ctx.device.cmdDraw( 264 + ctx.cmds, 265 + 3, 266 + 1, 267 + 0, 268 + 0, 269 + ); 110 270 } 111 271 112 272 pub fn as(self: *Window, comptime T: type) *T {
+31
src/triangle.slang
··· 1 + static float2 positions[3] = float2[]( 2 + float2(0.0, -0.5), 3 + float2(0.5, 0.5), 4 + float2(-0.5, 0.5) 5 + ); 6 + 7 + static float3 colors[3] = float3[]( 8 + float3(1.0, 0.0, 0.0), 9 + float3(0.0, 1.0, 0.0), 10 + float3(0.0, 0.0, 1.0) 11 + ); 12 + 13 + struct VertexOutput { 14 + float3 color; 15 + float4 sv_position : SV_Position; 16 + }; 17 + 18 + [shader("vertex")] 19 + VertexOutput vertMain(uint vid : SV_VertexID) { 20 + VertexOutput output; 21 + output.sv_position = float4(positions[vid], 0.0, 1.0); 22 + output.color = colors[vid]; 23 + return output; 24 + } 25 + 26 + [shader("fragment")] 27 + float4 fragMain(VertexOutput inVert) : SV_Target 28 + { 29 + float3 color = inVert.color; 30 + return float4(color, 1.0); 31 + }
+72 -7
src/vk_area.zig
··· 31 31 }; 32 32 33 33 /// Target rendering format 34 - const format: vk.Format = .b8g8r8a8_srgb; 34 + pub const format: vk.Format = .b8g8r8a8_srgb; 35 35 36 36 pub const RenderContext = struct { 37 37 device: *vk.DeviceProxy, ··· 278 278 // Choose and initialize a device 279 279 var found_device = false; 280 280 dev: for (try priv.instance.enumeratePhysicalDevicesAlloc(alloc)) |pdev| { 281 + // Check if the device supports the required Vulkan version 282 + const props = priv.instance.getPhysicalDeviceProperties(pdev); 283 + const required_version: u32 = @bitCast(vk.features.version_1_3.version); 284 + if (props.api_version < required_version) continue :dev; 285 + 281 286 // Check if the device supports all required extensions 282 287 const ext_props = try priv.instance.enumerateDeviceExtensionPropertiesAlloc( 283 288 pdev, ··· 343 348 344 349 var features_1_3: vk.PhysicalDeviceVulkan13Features = .{ 345 350 .dynamic_rendering = .true, 351 + .synchronization_2 = .true, 352 + }; 353 + var features_1_1: vk.PhysicalDeviceVulkan11Features = .{ 354 + .shader_draw_parameters = .true, 355 + .p_next = @ptrCast(&features_1_3), 346 356 }; 347 - 348 357 const features: vk.PhysicalDeviceFeatures2 = .{ 349 - .p_next = @ptrCast(&features_1_3), 358 + .p_next = @ptrCast(&features_1_1), 350 359 .features = .{}, 351 360 }; 352 361 ··· 425 434 ); 426 435 } 427 436 428 - // Commands 437 + // Draw! 429 438 try priv.device.beginCommandBuffer(priv.cmds, &.{}); 439 + 430 440 { 441 + // Transition into rendering layout 442 + priv.target.transition( 443 + priv.cmds, 444 + .undefined, 445 + .color_attachment_optimal, 446 + .{ .top_of_pipe_bit = true }, 447 + .{ .color_attachment_output_bit = true }, 448 + .{}, 449 + .{ .color_attachment_write_bit = true }, 450 + ); 451 + 431 452 const ctx: RenderContext = .{ 432 453 .cmds = priv.cmds, 433 454 .device = &priv.device, ··· 435 456 .extent = priv.extent, 436 457 }; 437 458 signals.render.impl.emit(self, null, .{&ctx}, null); 459 + 460 + // Transition into exporting layout 461 + priv.target.transition( 462 + priv.cmds, 463 + .color_attachment_optimal, 464 + .color_attachment_optimal, 465 + .{ .color_attachment_output_bit = true }, 466 + .{ .bottom_of_pipe_bit = true }, 467 + .{ .color_attachment_write_bit = true }, 468 + .{}, 469 + ); 438 470 } 439 471 try priv.device.endCommandBuffer(priv.cmds); 440 472 473 + // Wait for GPU 441 474 try priv.target.waitForFence(); 442 475 try priv.device.resetFences(1, &.{priv.target.fence}); 443 476 ··· 465 498 fn exportDmabufTexture(self: *Self) !?*gdk.Texture { 466 499 const priv = self.private(); 467 500 468 - // Requires 469 - // TODO: handle multiple DMABUF planes 470 501 const fd = try priv.device.getMemoryFdKHR(&.{ 471 502 .memory = priv.target.memory, 472 503 .handle_type = .{ .dma_buf_bit_ext = true }, ··· 501 532 dmabuf.setStride(plane, @intCast(layout.row_pitch)); 502 533 } 503 534 504 - // TODO: release data 505 535 const a = try std.heap.c_allocator.create(std.posix.fd_t); 506 536 a.* = fd; 507 537 return dmabuf.build(releaseDmabuf, a, null); ··· 656 686 self.device.destroyImageView(self.view, null); 657 687 self.device.destroyImage(self.image, null); 658 688 self.device.freeMemory(self.memory, null); 689 + } 690 + 691 + fn transition( 692 + self: Target, 693 + cmds: vk.CommandBuffer, 694 + old_layout: vk.ImageLayout, 695 + new_layout: vk.ImageLayout, 696 + src_stage_mask: vk.PipelineStageFlags2, 697 + dst_stage_mask: vk.PipelineStageFlags2, 698 + src_access_mask: vk.AccessFlags2, 699 + dst_access_mask: vk.AccessFlags2, 700 + ) void { 701 + const barriers = [_]vk.ImageMemoryBarrier2{.{ 702 + .src_stage_mask = src_stage_mask, 703 + .src_access_mask = src_access_mask, 704 + .dst_stage_mask = dst_stage_mask, 705 + .dst_access_mask = dst_access_mask, 706 + .old_layout = old_layout, 707 + .new_layout = new_layout, 708 + .src_queue_family_index = vk.QUEUE_FAMILY_IGNORED, 709 + .dst_queue_family_index = vk.QUEUE_FAMILY_IGNORED, 710 + .image = self.image, 711 + .subresource_range = .{ 712 + .aspect_mask = .{ .color_bit = true }, 713 + .base_mip_level = 0, 714 + .level_count = 1, 715 + .base_array_layer = 0, 716 + .layer_count = 1, 717 + }, 718 + }}; 719 + self.device.cmdPipelineBarrier2(cmds, &.{ 720 + .dependency_flags = .{}, 721 + .image_memory_barrier_count = barriers.len, 722 + .p_image_memory_barriers = &barriers, 723 + }); 659 724 } 660 725 }; 661 726 };
triangle.spv

This is a binary file and will not be displayed.