ff8b9f51
feat(renderer): render pass + pipeline + clear-and-present loop
a73x 2026-04-08 09:07
Commit message
src/main.zig
| Old | New | ||
|---|---|---|---|
| @@ -24,9 +24,44 @@ pub fn main() !void { | |||
| 24 | return runVulkanSmokeTest(alloc); | 24 | return runVulkanSmokeTest(alloc); |
| 25 | } | 25 | } |
| 26 | 26 | ||
| 27 | if (args.len >= 2 and std.mem.eql(u8, args[1], "--render-smoke-test")) { | ||
| 28 | return runRenderSmokeTest(alloc); | ||
| 29 | } | ||
| 30 | |||
| 27 | std.debug.print("waystty (run with --headless for CLI dump mode)\n", .{}); | 31 | std.debug.print("waystty (run with --headless for CLI dump mode)\n", .{}); |
| 28 | } | 32 | } |
| 29 | 33 | ||
| 34 | fn runRenderSmokeTest(alloc: std.mem.Allocator) !void { | ||
| 35 | var conn = try wayland_client.Connection.init(); | ||
| 36 | defer conn.deinit(); | ||
| 37 | std.debug.print("wayland connected\n", .{}); | ||
| 38 | |||
| 39 | const window = try conn.createWindow(alloc, "waystty-render-smoke"); | ||
| 40 | defer window.deinit(); | ||
| 41 | std.debug.print("window created (w={d} h={d})\n", .{ window.width, window.height }); | ||
| 42 | |||
| 43 | _ = conn.display.roundtrip(); | ||
| 44 | |||
| 45 | var ctx = try renderer.Context.init( | ||
| 46 | alloc, | ||
| 47 | @ptrCast(conn.display), | ||
| 48 | @ptrCast(window.surface), | ||
| 49 | window.width, | ||
| 50 | window.height, | ||
| 51 | ); | ||
| 52 | defer ctx.deinit(); | ||
| 53 | |||
| 54 | std.debug.print("rendering 60 frames...\n", .{}); | ||
| 55 | var i: u32 = 0; | ||
| 56 | while (i < 60) : (i += 1) { | ||
| 57 | _ = conn.display.dispatchPending(); | ||
| 58 | const t: f32 = @as(f32, @floatFromInt(i)) / 60.0; | ||
| 59 | try ctx.drawClear(.{ t, 0.5, 1.0 - t, 1.0 }); | ||
| 60 | } | ||
| 61 | _ = try ctx.vkd.deviceWaitIdle(ctx.device); | ||
| 62 | std.debug.print("done\n", .{}); | ||
| 63 | } | ||
| 64 | |||
| 30 | fn runVulkanSmokeTest(alloc: std.mem.Allocator) !void { | 65 | fn runVulkanSmokeTest(alloc: std.mem.Allocator) !void { |
| 31 | var conn = try wayland_client.Connection.init(); | 66 | var conn = try wayland_client.Connection.init(); |
| 32 | defer conn.deinit(); | 67 | defer conn.deinit(); |
src/renderer.zig
| Old | New | ||
|---|---|---|---|
| @@ -181,6 +181,25 @@ fn createSwapchain( | |||
| 181 | }; | 181 | }; |
| 182 | } | 182 | } |
| 183 | 183 | ||
| 184 | /// Push constants layout matching cell.vert | ||
| 185 | pub const PushConstants = extern struct { | ||
| 186 | viewport_size: [2]f32, | ||
| 187 | cell_size: [2]f32, | ||
| 188 | }; | ||
| 189 | |||
| 190 | /// Per-vertex data (binding 0, per-vertex rate) | ||
| 191 | pub const Vertex = extern struct { | ||
| 192 | unit_pos: [2]f32, // location 0 | ||
| 193 | }; | ||
| 194 | |||
| 195 | /// Per-instance data (binding 1, per-instance rate) | ||
| 196 | pub const Instance = extern struct { | ||
| 197 | cell_pos: [2]f32, // location 1 | ||
| 198 | uv_rect: [4]f32, // location 2 | ||
| 199 | fg: [4]f32, // location 3 | ||
| 200 | bg: [4]f32, // location 4 | ||
| 201 | }; | ||
| 202 | |||
| 184 | pub const Context = struct { | 203 | pub const Context = struct { |
| 185 | alloc: std.mem.Allocator, | 204 | alloc: std.mem.Allocator, |
| 186 | vkb: vk.BaseWrapper, | 205 | vkb: vk.BaseWrapper, |
| @@ -199,6 +218,23 @@ pub const Context = struct { | |||
| 199 | swapchain_extent: vk.Extent2D, | 218 | swapchain_extent: vk.Extent2D, |
| 200 | swapchain_images: []vk.Image, | 219 | swapchain_images: []vk.Image, |
| 201 | swapchain_image_views: []vk.ImageView, | 220 | swapchain_image_views: []vk.ImageView, |
| 221 | // Render pass + framebuffers | ||
| 222 | render_pass: vk.RenderPass, | ||
| 223 | framebuffers: []vk.Framebuffer, | ||
| 224 | // Descriptor set layout + pool + set | ||
| 225 | descriptor_set_layout: vk.DescriptorSetLayout, | ||
| 226 | descriptor_pool: vk.DescriptorPool, | ||
| 227 | descriptor_set: vk.DescriptorSet, | ||
| 228 | // Pipeline | ||
| 229 | pipeline_layout: vk.PipelineLayout, | ||
| 230 | pipeline: vk.Pipeline, | ||
| 231 | // Commands | ||
| 232 | command_pool: vk.CommandPool, | ||
| 233 | command_buffer: vk.CommandBuffer, | ||
| 234 | // Sync | ||
| 235 | image_available: vk.Semaphore, | ||
| 236 | render_finished: vk.Semaphore, | ||
| 237 | in_flight_fence: vk.Fence, | ||
| 202 | 238 | ||
| 203 | pub fn init( | 239 | pub fn init( |
| 204 | alloc: std.mem.Allocator, | 240 | alloc: std.mem.Allocator, |
| @@ -288,6 +324,264 @@ pub const Context = struct { | |||
| 288 | vkd.destroySwapchainKHR(device, sc.swapchain, null); | 324 | vkd.destroySwapchainKHR(device, sc.swapchain, null); |
| 289 | } | 325 | } |
| 290 | 326 | ||
| 327 | // Create render pass | ||
| 328 | const color_attachment = vk.AttachmentDescription{ | ||
| 329 | .format = sc.format, | ||
| 330 | .samples = .{ .@"1_bit" = true }, | ||
| 331 | .load_op = .clear, | ||
| 332 | .store_op = .store, | ||
| 333 | .stencil_load_op = .dont_care, | ||
| 334 | .stencil_store_op = .dont_care, | ||
| 335 | .initial_layout = .undefined, | ||
| 336 | .final_layout = .present_src_khr, | ||
| 337 | }; | ||
| 338 | |||
| 339 | const color_ref = vk.AttachmentReference{ | ||
| 340 | .attachment = 0, | ||
| 341 | .layout = .color_attachment_optimal, | ||
| 342 | }; | ||
| 343 | |||
| 344 | const subpass = vk.SubpassDescription{ | ||
| 345 | .pipeline_bind_point = .graphics, | ||
| 346 | .color_attachment_count = 1, | ||
| 347 | .p_color_attachments = @ptrCast(&color_ref), | ||
| 348 | }; | ||
| 349 | |||
| 350 | const dep = vk.SubpassDependency{ | ||
| 351 | .src_subpass = vk.SUBPASS_EXTERNAL, | ||
| 352 | .dst_subpass = 0, | ||
| 353 | .src_stage_mask = .{ .color_attachment_output_bit = true }, | ||
| 354 | .dst_stage_mask = .{ .color_attachment_output_bit = true }, | ||
| 355 | .src_access_mask = .{}, | ||
| 356 | .dst_access_mask = .{ .color_attachment_write_bit = true }, | ||
| 357 | }; | ||
| 358 | |||
| 359 | const render_pass = try vkd.createRenderPass(device, &vk.RenderPassCreateInfo{ | ||
| 360 | .attachment_count = 1, | ||
| 361 | .p_attachments = @ptrCast(&color_attachment), | ||
| 362 | .subpass_count = 1, | ||
| 363 | .p_subpasses = @ptrCast(&subpass), | ||
| 364 | .dependency_count = 1, | ||
| 365 | .p_dependencies = @ptrCast(&dep), | ||
| 366 | }, null); | ||
| 367 | errdefer vkd.destroyRenderPass(device, render_pass, null); | ||
| 368 | |||
| 369 | // Create framebuffers (one per swapchain image view) | ||
| 370 | const framebuffers = try alloc.alloc(vk.Framebuffer, sc.image_views.len); | ||
| 371 | errdefer alloc.free(framebuffers); | ||
| 372 | var fbs_created: usize = 0; | ||
| 373 | errdefer { | ||
| 374 | for (framebuffers[0..fbs_created]) |fb| vkd.destroyFramebuffer(device, fb, null); | ||
| 375 | } | ||
| 376 | for (sc.image_views, 0..) |view, i| { | ||
| 377 | framebuffers[i] = try vkd.createFramebuffer(device, &vk.FramebufferCreateInfo{ | ||
| 378 | .render_pass = render_pass, | ||
| 379 | .attachment_count = 1, | ||
| 380 | .p_attachments = @ptrCast(&view), | ||
| 381 | .width = sc.extent.width, | ||
| 382 | .height = sc.extent.height, | ||
| 383 | .layers = 1, | ||
| 384 | }, null); | ||
| 385 | fbs_created += 1; | ||
| 386 | } | ||
| 387 | |||
| 388 | // Create descriptor set layout (single combined image sampler for glyph atlas) | ||
| 389 | const dsl_binding = vk.DescriptorSetLayoutBinding{ | ||
| 390 | .binding = 0, | ||
| 391 | .descriptor_type = .combined_image_sampler, | ||
| 392 | .descriptor_count = 1, | ||
| 393 | .stage_flags = .{ .fragment_bit = true }, | ||
| 394 | }; | ||
| 395 | const descriptor_set_layout = try vkd.createDescriptorSetLayout(device, &vk.DescriptorSetLayoutCreateInfo{ | ||
| 396 | .binding_count = 1, | ||
| 397 | .p_bindings = @ptrCast(&dsl_binding), | ||
| 398 | }, null); | ||
| 399 | errdefer vkd.destroyDescriptorSetLayout(device, descriptor_set_layout, null); | ||
| 400 | |||
| 401 | // Create pipeline layout (push constants + descriptor set) | ||
| 402 | const push_range = vk.PushConstantRange{ | ||
| 403 | .stage_flags = .{ .vertex_bit = true }, | ||
| 404 | .offset = 0, | ||
| 405 | .size = @sizeOf(PushConstants), | ||
| 406 | }; | ||
| 407 | const pipeline_layout = try vkd.createPipelineLayout(device, &vk.PipelineLayoutCreateInfo{ | ||
| 408 | .set_layout_count = 1, | ||
| 409 | .p_set_layouts = @ptrCast(&descriptor_set_layout), | ||
| 410 | .push_constant_range_count = 1, | ||
| 411 | .p_push_constant_ranges = @ptrCast(&push_range), | ||
| 412 | }, null); | ||
| 413 | errdefer vkd.destroyPipelineLayout(device, pipeline_layout, null); | ||
| 414 | |||
| 415 | // Create shader modules | ||
| 416 | const vert_module = try vkd.createShaderModule(device, &vk.ShaderModuleCreateInfo{ | ||
| 417 | .code_size = cell_vert_spv.len, | ||
| 418 | .p_code = @ptrCast(@alignCast(cell_vert_spv.ptr)), | ||
| 419 | }, null); | ||
| 420 | defer vkd.destroyShaderModule(device, vert_module, null); | ||
| 421 | |||
| 422 | const frag_module = try vkd.createShaderModule(device, &vk.ShaderModuleCreateInfo{ | ||
| 423 | .code_size = cell_frag_spv.len, | ||
| 424 | .p_code = @ptrCast(@alignCast(cell_frag_spv.ptr)), | ||
| 425 | }, null); | ||
| 426 | defer vkd.destroyShaderModule(device, frag_module, null); | ||
| 427 | |||
| 428 | // Shader stages | ||
| 429 | const shader_stages = [_]vk.PipelineShaderStageCreateInfo{ | ||
| 430 | .{ | ||
| 431 | .stage = .{ .vertex_bit = true }, | ||
| 432 | .module = vert_module, | ||
| 433 | .p_name = "main", | ||
| 434 | }, | ||
| 435 | .{ | ||
| 436 | .stage = .{ .fragment_bit = true }, | ||
| 437 | .module = frag_module, | ||
| 438 | .p_name = "main", | ||
| 439 | }, | ||
| 440 | }; | ||
| 441 | |||
| 442 | // Vertex input | ||
| 443 | const binding_descs = [_]vk.VertexInputBindingDescription{ | ||
| 444 | .{ .binding = 0, .stride = @sizeOf(Vertex), .input_rate = .vertex }, | ||
| 445 | .{ .binding = 1, .stride = @sizeOf(Instance), .input_rate = .instance }, | ||
| 446 | }; | ||
| 447 | |||
| 448 | const attr_descs = [_]vk.VertexInputAttributeDescription{ | ||
| 449 | .{ .location = 0, .binding = 0, .format = .r32g32_sfloat, .offset = 0 }, | ||
| 450 | .{ .location = 1, .binding = 1, .format = .r32g32_sfloat, .offset = @offsetOf(Instance, "cell_pos") }, | ||
| 451 | .{ .location = 2, .binding = 1, .format = .r32g32b32a32_sfloat, .offset = @offsetOf(Instance, "uv_rect") }, | ||
| 452 | .{ .location = 3, .binding = 1, .format = .r32g32b32a32_sfloat, .offset = @offsetOf(Instance, "fg") }, | ||
| 453 | .{ .location = 4, .binding = 1, .format = .r32g32b32a32_sfloat, .offset = @offsetOf(Instance, "bg") }, | ||
| 454 | }; | ||
| 455 | |||
| 456 | const vertex_input_info = vk.PipelineVertexInputStateCreateInfo{ | ||
| 457 | .vertex_binding_description_count = binding_descs.len, | ||
| 458 | .p_vertex_binding_descriptions = &binding_descs, | ||
| 459 | .vertex_attribute_description_count = attr_descs.len, | ||
| 460 | .p_vertex_attribute_descriptions = &attr_descs, | ||
| 461 | }; | ||
| 462 | |||
| 463 | const input_assembly = vk.PipelineInputAssemblyStateCreateInfo{ | ||
| 464 | .topology = .triangle_list, | ||
| 465 | .primitive_restart_enable = .false, | ||
| 466 | }; | ||
| 467 | |||
| 468 | // Dynamic viewport + scissor (set at draw time) | ||
| 469 | const dynamic_states = [_]vk.DynamicState{ .viewport, .scissor }; | ||
| 470 | const dynamic_state = vk.PipelineDynamicStateCreateInfo{ | ||
| 471 | .dynamic_state_count = dynamic_states.len, | ||
| 472 | .p_dynamic_states = &dynamic_states, | ||
| 473 | }; | ||
| 474 | |||
| 475 | const viewport_state = vk.PipelineViewportStateCreateInfo{ | ||
| 476 | .viewport_count = 1, | ||
| 477 | .scissor_count = 1, | ||
| 478 | }; | ||
| 479 | |||
| 480 | const rasterizer = vk.PipelineRasterizationStateCreateInfo{ | ||
| 481 | .depth_clamp_enable = .false, | ||
| 482 | .rasterizer_discard_enable = .false, | ||
| 483 | .polygon_mode = .fill, | ||
| 484 | .cull_mode = .{ .back_bit = true }, | ||
| 485 | .front_face = .clockwise, | ||
| 486 | .depth_bias_enable = .false, | ||
| 487 | .depth_bias_constant_factor = 0.0, | ||
| 488 | .depth_bias_clamp = 0.0, | ||
| 489 | .depth_bias_slope_factor = 0.0, | ||
| 490 | .line_width = 1.0, | ||
| 491 | }; | ||
| 492 | |||
| 493 | const multisampling = vk.PipelineMultisampleStateCreateInfo{ | ||
| 494 | .rasterization_samples = .{ .@"1_bit" = true }, | ||
| 495 | .sample_shading_enable = .false, | ||
| 496 | .min_sample_shading = 1.0, | ||
| 497 | .alpha_to_coverage_enable = .false, | ||
| 498 | .alpha_to_one_enable = .false, | ||
| 499 | }; | ||
| 500 | |||
| 501 | const color_blend_attachment = vk.PipelineColorBlendAttachmentState{ | ||
| 502 | .blend_enable = .true, | ||
| 503 | .src_color_blend_factor = .src_alpha, | ||
| 504 | .dst_color_blend_factor = .one_minus_src_alpha, | ||
| 505 | .color_blend_op = .add, | ||
| 506 | .src_alpha_blend_factor = .one, | ||
| 507 | .dst_alpha_blend_factor = .zero, | ||
| 508 | .alpha_blend_op = .add, | ||
| 509 | .color_write_mask = .{ .r_bit = true, .g_bit = true, .b_bit = true, .a_bit = true }, | ||
| 510 | }; | ||
| 511 | |||
| 512 | const color_blend = vk.PipelineColorBlendStateCreateInfo{ | ||
| 513 | .logic_op_enable = .false, | ||
| 514 | .logic_op = .copy, | ||
| 515 | .attachment_count = 1, | ||
| 516 | .p_attachments = @ptrCast(&color_blend_attachment), | ||
| 517 | .blend_constants = .{ 0.0, 0.0, 0.0, 0.0 }, | ||
| 518 | }; | ||
| 519 | |||
| 520 | const pipeline_create_info = vk.GraphicsPipelineCreateInfo{ | ||
| 521 | .stage_count = shader_stages.len, | ||
| 522 | .p_stages = &shader_stages, | ||
| 523 | .p_vertex_input_state = &vertex_input_info, | ||
| 524 | .p_input_assembly_state = &input_assembly, | ||
| 525 | .p_viewport_state = &viewport_state, | ||
| 526 | .p_rasterization_state = &rasterizer, | ||
| 527 | .p_multisample_state = &multisampling, | ||
| 528 | .p_color_blend_state = &color_blend, | ||
| 529 | .p_dynamic_state = &dynamic_state, | ||
| 530 | .layout = pipeline_layout, | ||
| 531 | .render_pass = render_pass, | ||
| 532 | .subpass = 0, | ||
| 533 | .base_pipeline_index = -1, | ||
| 534 | }; | ||
| 535 | |||
| 536 | var pipeline: vk.Pipeline = undefined; | ||
| 537 | _ = try vkd.createGraphicsPipelines(device, .null_handle, 1, @ptrCast(&pipeline_create_info), null, @ptrCast(&pipeline)); | ||
| 538 | errdefer vkd.destroyPipeline(device, pipeline, null); | ||
| 539 | |||
| 540 | // Descriptor pool + set | ||
| 541 | const pool_size = vk.DescriptorPoolSize{ | ||
| 542 | .type = .combined_image_sampler, | ||
| 543 | .descriptor_count = 1, | ||
| 544 | }; | ||
| 545 | const descriptor_pool = try vkd.createDescriptorPool(device, &vk.DescriptorPoolCreateInfo{ | ||
| 546 | .max_sets = 1, | ||
| 547 | .pool_size_count = 1, | ||
| 548 | .p_pool_sizes = @ptrCast(&pool_size), | ||
| 549 | }, null); | ||
| 550 | errdefer vkd.destroyDescriptorPool(device, descriptor_pool, null); | ||
| 551 | |||
| 552 | var descriptor_set: vk.DescriptorSet = undefined; | ||
| 553 | try vkd.allocateDescriptorSets(device, &vk.DescriptorSetAllocateInfo{ | ||
| 554 | .descriptor_pool = descriptor_pool, | ||
| 555 | .descriptor_set_count = 1, | ||
| 556 | .p_set_layouts = @ptrCast(&descriptor_set_layout), | ||
| 557 | }, @ptrCast(&descriptor_set)); | ||
| 558 | |||
| 559 | // Command pool + buffer | ||
| 560 | const command_pool = try vkd.createCommandPool(device, &vk.CommandPoolCreateInfo{ | ||
| 561 | .flags = .{ .reset_command_buffer_bit = true }, | ||
| 562 | .queue_family_index = pd_info.graphics_queue_family, | ||
| 563 | }, null); | ||
| 564 | errdefer vkd.destroyCommandPool(device, command_pool, null); | ||
| 565 | |||
| 566 | var command_buffer: vk.CommandBuffer = undefined; | ||
| 567 | try vkd.allocateCommandBuffers(device, &vk.CommandBufferAllocateInfo{ | ||
| 568 | .command_pool = command_pool, | ||
| 569 | .level = .primary, | ||
| 570 | .command_buffer_count = 1, | ||
| 571 | }, @ptrCast(&command_buffer)); | ||
| 572 | |||
| 573 | // Sync objects | ||
| 574 | const image_available = try vkd.createSemaphore(device, &vk.SemaphoreCreateInfo{}, null); | ||
| 575 | errdefer vkd.destroySemaphore(device, image_available, null); | ||
| 576 | |||
| 577 | const render_finished = try vkd.createSemaphore(device, &vk.SemaphoreCreateInfo{}, null); | ||
| 578 | errdefer vkd.destroySemaphore(device, render_finished, null); | ||
| 579 | |||
| 580 | const in_flight_fence = try vkd.createFence(device, &vk.FenceCreateInfo{ | ||
| 581 | .flags = .{ .signaled_bit = true }, // start signaled so first wait returns immediately | ||
| 582 | }, null); | ||
| 583 | errdefer vkd.destroyFence(device, in_flight_fence, null); | ||
| 584 | |||
| 291 | return .{ | 585 | return .{ |
| 292 | .alloc = alloc, | 586 | .alloc = alloc, |
| 293 | .vkb = vkb, | 587 | .vkb = vkb, |
| @@ -306,18 +600,124 @@ pub const Context = struct { | |||
| 306 | .swapchain_extent = sc.extent, | 600 | .swapchain_extent = sc.extent, |
| 307 | .swapchain_images = sc.images, | 601 | .swapchain_images = sc.images, |
| 308 | .swapchain_image_views = sc.image_views, | 602 | .swapchain_image_views = sc.image_views, |
| 603 | .render_pass = render_pass, | ||
| 604 | .framebuffers = framebuffers, | ||
| 605 | .descriptor_set_layout = descriptor_set_layout, | ||
| 606 | .descriptor_pool = descriptor_pool, | ||
| 607 | .descriptor_set = descriptor_set, | ||
| 608 | .pipeline_layout = pipeline_layout, | ||
| 609 | .pipeline = pipeline, | ||
| 610 | .command_pool = command_pool, | ||
| 611 | .command_buffer = command_buffer, | ||
| 612 | .image_available = image_available, | ||
| 613 | .render_finished = render_finished, | ||
| 614 | .in_flight_fence = in_flight_fence, | ||
| 309 | }; | 615 | }; |
| 310 | } | 616 | } |
| 311 | 617 | ||
| 312 | pub fn deinit(self: *Context) void { | 618 | pub fn deinit(self: *Context) void { |
| 619 | // Wait for device to be idle before destroying anything | ||
| 620 | _ = self.vkd.deviceWaitIdle(self.device) catch {}; | ||
| 621 | |||
| 622 | // Sync objects | ||
| 623 | self.vkd.destroyFence(self.device, self.in_flight_fence, null); | ||
| 624 | self.vkd.destroySemaphore(self.device, self.render_finished, null); | ||
| 625 | self.vkd.destroySemaphore(self.device, self.image_available, null); | ||
| 626 | |||
| 627 | // Command pool (also frees command buffers) | ||
| 628 | self.vkd.destroyCommandPool(self.device, self.command_pool, null); | ||
| 629 | |||
| 630 | // Pipeline | ||
| 631 | self.vkd.destroyPipeline(self.device, self.pipeline, null); | ||
| 632 | self.vkd.destroyPipelineLayout(self.device, self.pipeline_layout, null); | ||
| 633 | |||
| 634 | // Descriptor pool (also frees descriptor sets) + layout | ||
| 635 | self.vkd.destroyDescriptorPool(self.device, self.descriptor_pool, null); | ||
| 636 | self.vkd.destroyDescriptorSetLayout(self.device, self.descriptor_set_layout, null); | ||
| 637 | |||
| 638 | // Framebuffers | ||
| 639 | for (self.framebuffers) |fb| self.vkd.destroyFramebuffer(self.device, fb, null); | ||
| 640 | self.alloc.free(self.framebuffers); | ||
| 641 | |||
| 642 | // Render pass | ||
| 643 | self.vkd.destroyRenderPass(self.device, self.render_pass, null); | ||
| 644 | |||
| 645 | // Swapchain | ||
| 313 | for (self.swapchain_image_views) |view| self.vkd.destroyImageView(self.device, view, null); | 646 | for (self.swapchain_image_views) |view| self.vkd.destroyImageView(self.device, view, null); |
| 314 | self.alloc.free(self.swapchain_image_views); | 647 | self.alloc.free(self.swapchain_image_views); |
| 315 | self.alloc.free(self.swapchain_images); | 648 | self.alloc.free(self.swapchain_images); |
| 316 | self.vkd.destroySwapchainKHR(self.device, self.swapchain, null); | 649 | self.vkd.destroySwapchainKHR(self.device, self.swapchain, null); |
| 650 | |||
| 317 | self.vkd.destroyDevice(self.device, null); | 651 | self.vkd.destroyDevice(self.device, null); |
| 318 | self.vki.destroySurfaceKHR(self.instance, self.surface, null); | 652 | self.vki.destroySurfaceKHR(self.instance, self.surface, null); |
| 319 | self.vki.destroyInstance(self.instance, null); | 653 | self.vki.destroyInstance(self.instance, null); |
| 320 | } | 654 | } |
| 655 | |||
| 656 | /// Record a command buffer that begins the render pass with the given clear color and presents. | ||
| 657 | /// Does not bind the pipeline or draw — just clear + present. | ||
| 658 | /// Blocks until the previous frame's fence signals. | ||
| 659 | pub fn drawClear(self: *Context, clear_color: [4]f32) !void { | ||
| 660 | // Wait for previous frame to finish | ||
| 661 | _ = try self.vkd.waitForFences(self.device, 1, @ptrCast(&self.in_flight_fence), .true, std.math.maxInt(u64)); | ||
| 662 | try self.vkd.resetFences(self.device, 1, @ptrCast(&self.in_flight_fence)); | ||
| 663 | |||
| 664 | // Acquire next image | ||
| 665 | const acquire = try self.vkd.acquireNextImageKHR( | ||
| 666 | self.device, | ||
| 667 | self.swapchain, | ||
| 668 | std.math.maxInt(u64), | ||
| 669 | self.image_available, | ||
| 670 | .null_handle, | ||
| 671 | ); | ||
| 672 | const image_index = acquire.image_index; | ||
| 673 | |||
| 674 | // Record command buffer | ||
| 675 | try self.vkd.resetCommandBuffer(self.command_buffer, .{}); | ||
| 676 | try self.vkd.beginCommandBuffer(self.command_buffer, &vk.CommandBufferBeginInfo{ | ||
| 677 | .flags = .{ .one_time_submit_bit = true }, | ||
| 678 | }); | ||
| 679 | |||
| 680 | const clear_value = vk.ClearValue{ | ||
| 681 | .color = .{ .float_32 = clear_color }, | ||
| 682 | }; | ||
| 683 | |||
| 684 | self.vkd.cmdBeginRenderPass(self.command_buffer, &vk.RenderPassBeginInfo{ | ||
| 685 | .render_pass = self.render_pass, | ||
| 686 | .framebuffer = self.framebuffers[image_index], | ||
| 687 | .render_area = .{ | ||
| 688 | .offset = .{ .x = 0, .y = 0 }, | ||
| 689 | .extent = self.swapchain_extent, | ||
| 690 | }, | ||
| 691 | .clear_value_count = 1, | ||
| 692 | .p_clear_values = @ptrCast(&clear_value), | ||
| 693 | }, .@"inline"); | ||
| 694 | |||
| 695 | // Don't bind pipeline or draw — just clear. | ||
| 696 | |||
| 697 | self.vkd.cmdEndRenderPass(self.command_buffer); | ||
| 698 | try self.vkd.endCommandBuffer(self.command_buffer); | ||
| 699 | |||
| 700 | // Submit | ||
| 701 | const wait_stage = vk.PipelineStageFlags{ .color_attachment_output_bit = true }; | ||
| 702 | try self.vkd.queueSubmit(self.graphics_queue, 1, @ptrCast(&vk.SubmitInfo{ | ||
| 703 | .wait_semaphore_count = 1, | ||
| 704 | .p_wait_semaphores = @ptrCast(&self.image_available), | ||
| 705 | .p_wait_dst_stage_mask = @ptrCast(&wait_stage), | ||
| 706 | .command_buffer_count = 1, | ||
| 707 | .p_command_buffers = @ptrCast(&self.command_buffer), | ||
| 708 | .signal_semaphore_count = 1, | ||
| 709 | .p_signal_semaphores = @ptrCast(&self.render_finished), | ||
| 710 | }), self.in_flight_fence); | ||
| 711 | |||
| 712 | // Present | ||
| 713 | _ = try self.vkd.queuePresentKHR(self.present_queue, &vk.PresentInfoKHR{ | ||
| 714 | .wait_semaphore_count = 1, | ||
| 715 | .p_wait_semaphores = @ptrCast(&self.render_finished), | ||
| 716 | .swapchain_count = 1, | ||
| 717 | .p_swapchains = @ptrCast(&self.swapchain), | ||
| 718 | .p_image_indices = @ptrCast(&image_index), | ||
| 719 | }); | ||
| 720 | } | ||
| 321 | }; | 721 | }; |
| 322 | 722 | ||
| 323 | test "vulkan module imports" { | 723 | test "vulkan module imports" { |