2d429b7f
Add offscreen render target + readback to renderer
a73x 2026-04-17 15:14
Commit message
src/renderer.zig
| Old | New | ||
|---|---|---|---|
| @@ -424,6 +424,125 @@ fn swapchainNeedsRebuild(result: vk.Result) bool { | |||
| 424 | return result == .suboptimal_khr; | 424 | return result == .suboptimal_khr; |
| 425 | } | 425 | } |
| 426 | 426 | ||
| 427 | /// Offscreen color attachment + readback staging buffer. | ||
| 428 | /// The image is allocated with COLOR_ATTACHMENT_BIT | TRANSFER_SRC_BIT so the | ||
| 429 | /// renderer can draw into it and then copy it into the host-visible readback | ||
| 430 | /// buffer. Created via `createOffscreen` and freed with `destroyOffscreen`. | ||
| 431 | pub const OffscreenTarget = struct { | ||
| 432 | width: u32, | ||
| 433 | height: u32, | ||
| 434 | format: vk.Format, | ||
| 435 | image: vk.Image, | ||
| 436 | memory: vk.DeviceMemory, | ||
| 437 | view: vk.ImageView, | ||
| 438 | framebuffer: vk.Framebuffer, | ||
| 439 | readback_buffer: vk.Buffer, | ||
| 440 | readback_memory: vk.DeviceMemory, | ||
| 441 | readback_size: u64, | ||
| 442 | }; | ||
| 443 | |||
| 444 | /// Allocate an offscreen color-attachment image plus matching readback buffer. | ||
| 445 | /// The framebuffer is compatible with `render_pass` — pass the same render pass | ||
| 446 | /// the swapchain uses so the existing pipeline is reusable. | ||
| 447 | pub fn createOffscreen( | ||
| 448 | vki: vk.InstanceWrapper, | ||
| 449 | vkd: vk.DeviceWrapper, | ||
| 450 | physical: vk.PhysicalDevice, | ||
| 451 | device: vk.Device, | ||
| 452 | render_pass: vk.RenderPass, | ||
| 453 | format: vk.Format, | ||
| 454 | width: u32, | ||
| 455 | height: u32, | ||
| 456 | ) !OffscreenTarget { | ||
| 457 | // 1. Color attachment image with TRANSFER_SRC_BIT | COLOR_ATTACHMENT_BIT | ||
| 458 | const image = try vkd.createImage(device, &vk.ImageCreateInfo{ | ||
| 459 | .image_type = .@"2d", | ||
| 460 | .format = format, | ||
| 461 | .extent = .{ .width = width, .height = height, .depth = 1 }, | ||
| 462 | .mip_levels = 1, | ||
| 463 | .array_layers = 1, | ||
| 464 | .samples = .{ .@"1_bit" = true }, | ||
| 465 | .tiling = .optimal, | ||
| 466 | .usage = .{ .color_attachment_bit = true, .transfer_src_bit = true }, | ||
| 467 | .sharing_mode = .exclusive, | ||
| 468 | .initial_layout = .undefined, | ||
| 469 | }, null); | ||
| 470 | errdefer vkd.destroyImage(device, image, null); | ||
| 471 | |||
| 472 | // 2. Device-local memory bound | ||
| 473 | const img_reqs = vkd.getImageMemoryRequirements(device, image); | ||
| 474 | const img_mem_idx = try findMemoryType(vki, physical, img_reqs.memory_type_bits, .{ .device_local_bit = true }); | ||
| 475 | const memory = try vkd.allocateMemory(device, &vk.MemoryAllocateInfo{ | ||
| 476 | .allocation_size = img_reqs.size, | ||
| 477 | .memory_type_index = img_mem_idx, | ||
| 478 | }, null); | ||
| 479 | errdefer vkd.freeMemory(device, memory, null); | ||
| 480 | try vkd.bindImageMemory(device, image, memory, 0); | ||
| 481 | |||
| 482 | // 3. ImageView + Framebuffer (using the provided render_pass) | ||
| 483 | const view = try vkd.createImageView(device, &vk.ImageViewCreateInfo{ | ||
| 484 | .image = image, | ||
| 485 | .view_type = .@"2d", | ||
| 486 | .format = format, | ||
| 487 | .components = .{ .r = .identity, .g = .identity, .b = .identity, .a = .identity }, | ||
| 488 | .subresource_range = .{ | ||
| 489 | .aspect_mask = .{ .color_bit = true }, | ||
| 490 | .base_mip_level = 0, | ||
| 491 | .level_count = 1, | ||
| 492 | .base_array_layer = 0, | ||
| 493 | .layer_count = 1, | ||
| 494 | }, | ||
| 495 | }, null); | ||
| 496 | errdefer vkd.destroyImageView(device, view, null); | ||
| 497 | |||
| 498 | const framebuffer = try vkd.createFramebuffer(device, &vk.FramebufferCreateInfo{ | ||
| 499 | .render_pass = render_pass, | ||
| 500 | .attachment_count = 1, | ||
| 501 | .p_attachments = @ptrCast(&view), | ||
| 502 | .width = width, | ||
| 503 | .height = height, | ||
| 504 | .layers = 1, | ||
| 505 | }, null); | ||
| 506 | errdefer vkd.destroyFramebuffer(device, framebuffer, null); | ||
| 507 | |||
| 508 | // 4. Host-visible readback buffer (TRANSFER_DST, width*height*4 bytes) | ||
| 509 | const readback_size: u64 = @as(u64, width) * @as(u64, height) * 4; | ||
| 510 | const readback = try createHostVisibleBuffer( | ||
| 511 | vki, | ||
| 512 | physical, | ||
| 513 | vkd, | ||
| 514 | device, | ||
| 515 | @intCast(readback_size), | ||
| 516 | .{ .transfer_dst_bit = true }, | ||
| 517 | ); | ||
| 518 | errdefer { | ||
| 519 | vkd.destroyBuffer(device, readback.buffer, null); | ||
| 520 | vkd.freeMemory(device, readback.memory, null); | ||
| 521 | } | ||
| 522 | |||
| 523 | return .{ | ||
| 524 | .width = width, | ||
| 525 | .height = height, | ||
| 526 | .format = format, | ||
| 527 | .image = image, | ||
| 528 | .memory = memory, | ||
| 529 | .view = view, | ||
| 530 | .framebuffer = framebuffer, | ||
| 531 | .readback_buffer = readback.buffer, | ||
| 532 | .readback_memory = readback.memory, | ||
| 533 | .readback_size = readback_size, | ||
| 534 | }; | ||
| 535 | } | ||
| 536 | |||
| 537 | pub fn destroyOffscreen(vkd: vk.DeviceWrapper, device: vk.Device, t: OffscreenTarget) void { | ||
| 538 | vkd.destroyFramebuffer(device, t.framebuffer, null); | ||
| 539 | vkd.destroyImageView(device, t.view, null); | ||
| 540 | vkd.destroyImage(device, t.image, null); | ||
| 541 | vkd.freeMemory(device, t.memory, null); | ||
| 542 | vkd.destroyBuffer(device, t.readback_buffer, null); | ||
| 543 | vkd.freeMemory(device, t.readback_memory, null); | ||
| 544 | } | ||
| 545 | |||
| 427 | pub const Context = struct { | 546 | pub const Context = struct { |
| 428 | alloc: std.mem.Allocator, | 547 | alloc: std.mem.Allocator, |
| 429 | vkb: vk.BaseWrapper, | 548 | vkb: vk.BaseWrapper, |
| @@ -479,6 +598,9 @@ pub const Context = struct { | |||
| 479 | // Dedicated transfer command buffer + fence | 598 | // Dedicated transfer command buffer + fence |
| 480 | atlas_transfer_cb: vk.CommandBuffer, | 599 | atlas_transfer_cb: vk.CommandBuffer, |
| 481 | atlas_transfer_fence: vk.Fence, | 600 | atlas_transfer_fence: vk.Fence, |
| 601 | // Dedicated capture command buffer + fence (used by renderToOffscreen) | ||
| 602 | capture_cmd: vk.CommandBuffer, | ||
| 603 | capture_fence: vk.Fence, | ||
| 482 | 604 | ||
| 483 | pub fn init( | 605 | pub fn init( |
| 484 | alloc: std.mem.Allocator, | 606 | alloc: std.mem.Allocator, |
| @@ -935,6 +1057,19 @@ pub const Context = struct { | |||
| 935 | }, null); | 1057 | }, null); |
| 936 | errdefer vkd.destroyFence(device, atlas_transfer_fence, null); | 1058 | errdefer vkd.destroyFence(device, atlas_transfer_fence, null); |
| 937 | 1059 | ||
| 1060 | // --- Dedicated capture (offscreen render + readback) command buffer + fence --- | ||
| 1061 | var capture_cmd: vk.CommandBuffer = undefined; | ||
| 1062 | try vkd.allocateCommandBuffers(device, &vk.CommandBufferAllocateInfo{ | ||
| 1063 | .command_pool = command_pool, | ||
| 1064 | .level = .primary, | ||
| 1065 | .command_buffer_count = 1, | ||
| 1066 | }, @ptrCast(&capture_cmd)); | ||
| 1067 | |||
| 1068 | const capture_fence = try vkd.createFence(device, &vk.FenceCreateInfo{ | ||
| 1069 | .flags = .{ .signaled_bit = true }, | ||
| 1070 | }, null); | ||
| 1071 | errdefer vkd.destroyFence(device, capture_fence, null); | ||
| 1072 | |||
| 938 | // Bind atlas to descriptor set | 1073 | // Bind atlas to descriptor set |
| 939 | const img_info = vk.DescriptorImageInfo{ | 1074 | const img_info = vk.DescriptorImageInfo{ |
| 940 | .sampler = atlas_sampler, | 1075 | .sampler = atlas_sampler, |
| @@ -997,6 +1132,8 @@ pub const Context = struct { | |||
| 997 | .atlas_staging_memory = atlas_staging.memory, | 1132 | .atlas_staging_memory = atlas_staging.memory, |
| 998 | .atlas_transfer_cb = atlas_transfer_cb, | 1133 | .atlas_transfer_cb = atlas_transfer_cb, |
| 999 | .atlas_transfer_fence = atlas_transfer_fence, | 1134 | .atlas_transfer_fence = atlas_transfer_fence, |
| 1135 | .capture_cmd = capture_cmd, | ||
| 1136 | .capture_fence = capture_fence, | ||
| 1000 | }; | 1137 | }; |
| 1001 | } | 1138 | } |
| 1002 | 1139 | ||
| @@ -1012,6 +1149,7 @@ pub const Context = struct { | |||
| 1012 | self.vkd.destroyBuffer(self.device, self.atlas_staging_buffer, null); | 1149 | self.vkd.destroyBuffer(self.device, self.atlas_staging_buffer, null); |
| 1013 | self.vkd.freeMemory(self.device, self.atlas_staging_memory, null); | 1150 | self.vkd.freeMemory(self.device, self.atlas_staging_memory, null); |
| 1014 | self.vkd.destroyFence(self.device, self.atlas_transfer_fence, null); | 1151 | self.vkd.destroyFence(self.device, self.atlas_transfer_fence, null); |
| 1152 | self.vkd.destroyFence(self.device, self.capture_fence, null); | ||
| 1015 | self.vkd.destroyBuffer(self.device, self.instance_buffer, null); | 1153 | self.vkd.destroyBuffer(self.device, self.instance_buffer, null); |
| 1016 | self.vkd.freeMemory(self.device, self.instance_memory, null); | 1154 | self.vkd.freeMemory(self.device, self.instance_memory, null); |
| 1017 | self.vkd.destroyBuffer(self.device, self.quad_vertex_buffer, null); | 1155 | self.vkd.destroyBuffer(self.device, self.quad_vertex_buffer, null); |
| @@ -1486,6 +1624,67 @@ pub const Context = struct { | |||
| 1486 | return false; | 1624 | return false; |
| 1487 | } | 1625 | } |
| 1488 | 1626 | ||
| 1627 | /// Shared "bind pipeline + push constants + vertex/instance buffers + | ||
| 1628 | /// dynamic viewport/scissor + drawInstanced" block used by both the | ||
| 1629 | /// swapchain draw path (`drawCells`) and the offscreen draw path | ||
| 1630 | /// (`renderToOffscreen`). | ||
| 1631 | /// | ||
| 1632 | /// The caller is responsible for recording the surrounding | ||
| 1633 | /// `cmdBeginRenderPass`/`cmdEndRenderPass` pair with the appropriate | ||
| 1634 | /// framebuffer and render area. | ||
| 1635 | fn recordDrawCommands( | ||
| 1636 | self: *Context, | ||
| 1637 | cmd: vk.CommandBuffer, | ||
| 1638 | extent: vk.Extent2D, | ||
| 1639 | instance_count: u32, | ||
| 1640 | push: PushConstants, | ||
| 1641 | ) void { | ||
| 1642 | self.vkd.cmdBindPipeline(cmd, .graphics, self.pipeline); | ||
| 1643 | |||
| 1644 | // Dynamic viewport + scissor | ||
| 1645 | const viewport = vk.Viewport{ | ||
| 1646 | .x = 0.0, | ||
| 1647 | .y = 0.0, | ||
| 1648 | .width = @floatFromInt(extent.width), | ||
| 1649 | .height = @floatFromInt(extent.height), | ||
| 1650 | .min_depth = 0.0, | ||
| 1651 | .max_depth = 1.0, | ||
| 1652 | }; | ||
| 1653 | self.vkd.cmdSetViewport(cmd, 0, 1, @ptrCast(&viewport)); | ||
| 1654 | |||
| 1655 | const scissor = vk.Rect2D{ | ||
| 1656 | .offset = .{ .x = 0, .y = 0 }, | ||
| 1657 | .extent = extent, | ||
| 1658 | }; | ||
| 1659 | self.vkd.cmdSetScissor(cmd, 0, 1, @ptrCast(&scissor)); | ||
| 1660 | |||
| 1661 | // Push constants | ||
| 1662 | self.vkd.cmdPushConstants( | ||
| 1663 | cmd, | ||
| 1664 | self.pipeline_layout, | ||
| 1665 | .{ .vertex_bit = true, .fragment_bit = true }, | ||
| 1666 | 0, | ||
| 1667 | @sizeOf(PushConstants), | ||
| 1668 | @ptrCast(&push), | ||
| 1669 | ); | ||
| 1670 | |||
| 1671 | // Bind descriptor set (atlas sampler) | ||
| 1672 | self.vkd.cmdBindDescriptorSets( | ||
| 1673 | cmd, | ||
| 1674 | .graphics, | ||
| 1675 | self.pipeline_layout, | ||
| 1676 | 0, 1, @ptrCast(&self.descriptor_set), | ||
| 1677 | 0, null, | ||
| 1678 | ); | ||
| 1679 | |||
| 1680 | // Bind vertex buffers: binding 0 = quad, binding 1 = instances | ||
| 1681 | const buffers = [_]vk.Buffer{ self.quad_vertex_buffer, self.instance_buffer }; | ||
| 1682 | const offsets = [_]vk.DeviceSize{ 0, 0 }; | ||
| 1683 | self.vkd.cmdBindVertexBuffers(cmd, 0, 2, &buffers, &offsets); | ||
| 1684 | |||
| 1685 | self.vkd.cmdDraw(cmd, 6, instance_count, 0, 0); | ||
| 1686 | } | ||
| 1687 | |||
| 1489 | /// Full draw pass: bind pipeline, push constants, vertex + instance buffers, draw, present. | 1688 | /// Full draw pass: bind pipeline, push constants, vertex + instance buffers, draw, present. |
| 1490 | pub fn drawCells( | 1689 | pub fn drawCells( |
| 1491 | self: *Context, | 1690 | self: *Context, |
| @@ -1531,26 +1730,6 @@ pub const Context = struct { | |||
| 1531 | .p_clear_values = @ptrCast(&clear_value), | 1730 | .p_clear_values = @ptrCast(&clear_value), |
| 1532 | }, .@"inline"); | 1731 | }, .@"inline"); |
| 1533 | 1732 | ||
| 1534 | self.vkd.cmdBindPipeline(self.command_buffer, .graphics, self.pipeline); | ||
| 1535 | |||
| 1536 | // Dynamic viewport + scissor | ||
| 1537 | const viewport = vk.Viewport{ | ||
| 1538 | .x = 0.0, | ||
| 1539 | .y = 0.0, | ||
| 1540 | .width = @floatFromInt(self.swapchain_extent.width), | ||
| 1541 | .height = @floatFromInt(self.swapchain_extent.height), | ||
| 1542 | .min_depth = 0.0, | ||
| 1543 | .max_depth = 1.0, | ||
| 1544 | }; | ||
| 1545 | self.vkd.cmdSetViewport(self.command_buffer, 0, 1, @ptrCast(&viewport)); | ||
| 1546 | |||
| 1547 | const scissor = vk.Rect2D{ | ||
| 1548 | .offset = .{ .x = 0, .y = 0 }, | ||
| 1549 | .extent = self.swapchain_extent, | ||
| 1550 | }; | ||
| 1551 | self.vkd.cmdSetScissor(self.command_buffer, 0, 1, @ptrCast(&scissor)); | ||
| 1552 | |||
| 1553 | // Push constants | ||
| 1554 | const pc = PushConstants{ | 1733 | const pc = PushConstants{ |
| 1555 | .viewport_size = .{ | 1734 | .viewport_size = .{ |
| 1556 | @floatFromInt(self.swapchain_extent.width), | 1735 | @floatFromInt(self.swapchain_extent.width), |
| @@ -1559,30 +1738,7 @@ pub const Context = struct { | |||
| 1559 | .cell_size = cell_size, | 1738 | .cell_size = cell_size, |
| 1560 | .coverage_params = coverage_params, | 1739 | .coverage_params = coverage_params, |
| 1561 | }; | 1740 | }; |
| 1562 | self.vkd.cmdPushConstants( | 1741 | self.recordDrawCommands(self.command_buffer, self.swapchain_extent, instance_count, pc); |
| 1563 | self.command_buffer, | ||
| 1564 | self.pipeline_layout, | ||
| 1565 | .{ .vertex_bit = true, .fragment_bit = true }, | ||
| 1566 | 0, | ||
| 1567 | @sizeOf(PushConstants), | ||
| 1568 | @ptrCast(&pc), | ||
| 1569 | ); | ||
| 1570 | |||
| 1571 | // Bind descriptor set (atlas sampler) | ||
| 1572 | self.vkd.cmdBindDescriptorSets( | ||
| 1573 | self.command_buffer, | ||
| 1574 | .graphics, | ||
| 1575 | self.pipeline_layout, | ||
| 1576 | 0, 1, @ptrCast(&self.descriptor_set), | ||
| 1577 | 0, null, | ||
| 1578 | ); | ||
| 1579 | |||
| 1580 | // Bind vertex buffers: binding 0 = quad, binding 1 = instances | ||
| 1581 | const buffers = [_]vk.Buffer{ self.quad_vertex_buffer, self.instance_buffer }; | ||
| 1582 | const offsets = [_]vk.DeviceSize{ 0, 0 }; | ||
| 1583 | self.vkd.cmdBindVertexBuffers(self.command_buffer, 0, 2, &buffers, &offsets); | ||
| 1584 | |||
| 1585 | self.vkd.cmdDraw(self.command_buffer, 6, instance_count, 0, 0); | ||
| 1586 | 1742 | ||
| 1587 | self.vkd.cmdEndRenderPass(self.command_buffer); | 1743 | self.vkd.cmdEndRenderPass(self.command_buffer); |
| 1588 | try self.vkd.endCommandBuffer(self.command_buffer); | 1744 | try self.vkd.endCommandBuffer(self.command_buffer); |
| @@ -1612,6 +1768,176 @@ pub const Context = struct { | |||
| 1612 | }; | 1768 | }; |
| 1613 | if (swapchainNeedsRebuild(present_result)) return error.OutOfDateKHR; | 1769 | if (swapchainNeedsRebuild(present_result)) return error.OutOfDateKHR; |
| 1614 | } | 1770 | } |
| 1771 | |||
| 1772 | /// Render `instance_data` into the offscreen target and copy the rendered | ||
| 1773 | /// image into the target's host-visible readback buffer. | ||
| 1774 | /// | ||
| 1775 | /// After this call returns, `target.readback_buffer` contains the raw | ||
| 1776 | /// bytes in the target's native format (typically BGRA8). Use | ||
| 1777 | /// `readbackOffscreen` to pull them out as RGBA8. | ||
| 1778 | pub fn renderToOffscreen( | ||
| 1779 | self: *Context, | ||
| 1780 | target: *const OffscreenTarget, | ||
| 1781 | instance_data: []const Instance, | ||
| 1782 | push: PushConstants, | ||
| 1783 | ) !void { | ||
| 1784 | // 1. Upload instances (same path drawCells uses) | ||
| 1785 | try self.uploadInstances(instance_data); | ||
| 1786 | |||
| 1787 | // 2. Reset + begin capture command buffer | ||
| 1788 | _ = try self.vkd.waitForFences(self.device, 1, @ptrCast(&self.capture_fence), .true, std.math.maxInt(u64)); | ||
| 1789 | try self.vkd.resetFences(self.device, 1, @ptrCast(&self.capture_fence)); | ||
| 1790 | |||
| 1791 | try self.vkd.resetCommandBuffer(self.capture_cmd, .{}); | ||
| 1792 | try self.vkd.beginCommandBuffer(self.capture_cmd, &vk.CommandBufferBeginInfo{ | ||
| 1793 | .flags = .{ .one_time_submit_bit = true }, | ||
| 1794 | }); | ||
| 1795 | |||
| 1796 | // 3. Transition target.image UNDEFINED -> COLOR_ATTACHMENT_OPTIMAL. | ||
| 1797 | // The render pass's `initial_layout = .undefined` technically | ||
| 1798 | // accepts the image in any layout, but an explicit barrier makes | ||
| 1799 | // the access masks/stage masks unambiguous. | ||
| 1800 | const to_color = vk.ImageMemoryBarrier{ | ||
| 1801 | .src_access_mask = .{}, | ||
| 1802 | .dst_access_mask = .{ .color_attachment_write_bit = true }, | ||
| 1803 | .old_layout = .undefined, | ||
| 1804 | .new_layout = .color_attachment_optimal, | ||
| 1805 | .src_queue_family_index = vk.QUEUE_FAMILY_IGNORED, | ||
| 1806 | .dst_queue_family_index = vk.QUEUE_FAMILY_IGNORED, | ||
| 1807 | .image = target.image, | ||
| 1808 | .subresource_range = .{ | ||
| 1809 | .aspect_mask = .{ .color_bit = true }, | ||
| 1810 | .base_mip_level = 0, | ||
| 1811 | .level_count = 1, | ||
| 1812 | .base_array_layer = 0, | ||
| 1813 | .layer_count = 1, | ||
| 1814 | }, | ||
| 1815 | }; | ||
| 1816 | self.vkd.cmdPipelineBarrier( | ||
| 1817 | self.capture_cmd, | ||
| 1818 | .{ .top_of_pipe_bit = true }, | ||
| 1819 | .{ .color_attachment_output_bit = true }, | ||
| 1820 | .{}, | ||
| 1821 | 0, null, | ||
| 1822 | 0, null, | ||
| 1823 | 1, @ptrCast(&to_color), | ||
| 1824 | ); | ||
| 1825 | |||
| 1826 | // 4. Begin the render pass on target.framebuffer / extent | ||
| 1827 | const clear_value = vk.ClearValue{ .color = .{ .float_32 = .{ 0.0, 0.0, 0.0, 1.0 } } }; | ||
| 1828 | const extent = vk.Extent2D{ .width = target.width, .height = target.height }; | ||
| 1829 | self.vkd.cmdBeginRenderPass(self.capture_cmd, &vk.RenderPassBeginInfo{ | ||
| 1830 | .render_pass = self.render_pass, | ||
| 1831 | .framebuffer = target.framebuffer, | ||
| 1832 | .render_area = .{ | ||
| 1833 | .offset = .{ .x = 0, .y = 0 }, | ||
| 1834 | .extent = extent, | ||
| 1835 | }, | ||
| 1836 | .clear_value_count = 1, | ||
| 1837 | .p_clear_values = @ptrCast(&clear_value), | ||
| 1838 | }, .@"inline"); | ||
| 1839 | |||
| 1840 | // 5. Shared draw commands (same as drawCells) | ||
| 1841 | self.recordDrawCommands(self.capture_cmd, extent, @intCast(instance_data.len), push); | ||
| 1842 | |||
| 1843 | // 6. End render pass (the pass's final_layout is .present_src_khr) | ||
| 1844 | self.vkd.cmdEndRenderPass(self.capture_cmd); | ||
| 1845 | |||
| 1846 | // 7. Transition target.image PRESENT_SRC_KHR -> TRANSFER_SRC_OPTIMAL | ||
| 1847 | // The render pass forced the final layout to .present_src_khr | ||
| 1848 | // (it's the same render pass the swapchain uses). Barrier from | ||
| 1849 | // there to TRANSFER_SRC before cmdCopyImageToBuffer. | ||
| 1850 | const to_transfer = vk.ImageMemoryBarrier{ | ||
| 1851 | .src_access_mask = .{ .color_attachment_write_bit = true }, | ||
| 1852 | .dst_access_mask = .{ .transfer_read_bit = true }, | ||
| 1853 | .old_layout = .present_src_khr, | ||
| 1854 | .new_layout = .transfer_src_optimal, | ||
| 1855 | .src_queue_family_index = vk.QUEUE_FAMILY_IGNORED, | ||
| 1856 | .dst_queue_family_index = vk.QUEUE_FAMILY_IGNORED, | ||
| 1857 | .image = target.image, | ||
| 1858 | .subresource_range = .{ | ||
| 1859 | .aspect_mask = .{ .color_bit = true }, | ||
| 1860 | .base_mip_level = 0, | ||
| 1861 | .level_count = 1, | ||
| 1862 | .base_array_layer = 0, | ||
| 1863 | .layer_count = 1, | ||
| 1864 | }, | ||
| 1865 | }; | ||
| 1866 | self.vkd.cmdPipelineBarrier( | ||
| 1867 | self.capture_cmd, | ||
| 1868 | .{ .color_attachment_output_bit = true }, | ||
| 1869 | .{ .transfer_bit = true }, | ||
| 1870 | .{}, | ||
| 1871 | 0, null, | ||
| 1872 | 0, null, | ||
| 1873 | 1, @ptrCast(&to_transfer), | ||
| 1874 | ); | ||
| 1875 | |||
| 1876 | // 8. Copy image -> readback buffer | ||
| 1877 | const region = vk.BufferImageCopy{ | ||
| 1878 | .buffer_offset = 0, | ||
| 1879 | .buffer_row_length = 0, | ||
| 1880 | .buffer_image_height = 0, | ||
| 1881 | .image_subresource = .{ | ||
| 1882 | .aspect_mask = .{ .color_bit = true }, | ||
| 1883 | .mip_level = 0, | ||
| 1884 | .base_array_layer = 0, | ||
| 1885 | .layer_count = 1, | ||
| 1886 | }, | ||
| 1887 | .image_offset = .{ .x = 0, .y = 0, .z = 0 }, | ||
| 1888 | .image_extent = .{ .width = target.width, .height = target.height, .depth = 1 }, | ||
| 1889 | }; | ||
| 1890 | self.vkd.cmdCopyImageToBuffer( | ||
| 1891 | self.capture_cmd, | ||
| 1892 | target.image, | ||
| 1893 | .transfer_src_optimal, | ||
| 1894 | target.readback_buffer, | ||
| 1895 | 1, | ||
| 1896 | @ptrCast(®ion), | ||
| 1897 | ); | ||
| 1898 | |||
| 1899 | // 9. End + submit + wait | ||
| 1900 | try self.vkd.endCommandBuffer(self.capture_cmd); | ||
| 1901 | try self.vkd.queueSubmit(self.graphics_queue, 1, @ptrCast(&vk.SubmitInfo{ | ||
| 1902 | .command_buffer_count = 1, | ||
| 1903 | .p_command_buffers = @ptrCast(&self.capture_cmd), | ||
| 1904 | }), self.capture_fence); | ||
| 1905 | _ = try self.vkd.waitForFences(self.device, 1, @ptrCast(&self.capture_fence), .true, std.math.maxInt(u64)); | ||
| 1906 | } | ||
| 1907 | |||
| 1908 | /// Read the offscreen target's readback buffer into `out_rgba`. | ||
| 1909 | /// Converts BGRA (the swapchain/native format) to RGBA and forces | ||
| 1910 | /// alpha to 0xFF. Caller must ensure a prior `renderToOffscreen` has | ||
| 1911 | /// finished (it waits on the capture fence, so simple back-to-back | ||
| 1912 | /// call is safe). | ||
| 1913 | pub fn readbackOffscreen( | ||
| 1914 | self: *Context, | ||
| 1915 | target: *const OffscreenTarget, | ||
| 1916 | out_rgba: []u8, | ||
| 1917 | ) !void { | ||
| 1918 | if (out_rgba.len < target.readback_size) return error.BufferTooSmall; | ||
| 1919 | |||
| 1920 | const mapped = try self.vkd.mapMemory( | ||
| 1921 | self.device, | ||
| 1922 | target.readback_memory, | ||
| 1923 | 0, | ||
| 1924 | @intCast(target.readback_size), | ||
| 1925 | .{}, | ||
| 1926 | ); | ||
| 1927 | defer self.vkd.unmapMemory(self.device, target.readback_memory); | ||
| 1928 | |||
| 1929 | const src = @as([*]const u8, @ptrCast(mapped))[0..target.readback_size]; | ||
| 1930 | const pixel_count: usize = @intCast(@as(u64, target.width) * @as(u64, target.height)); | ||
| 1931 | var i: usize = 0; | ||
| 1932 | while (i < pixel_count) : (i += 1) { | ||
| 1933 | const o = i * 4; | ||
| 1934 | // Source is BGRA8 (swapchain-native); produce RGBA8, force alpha=0xFF. | ||
| 1935 | out_rgba[o + 0] = src[o + 2]; // R <- B | ||
| 1936 | out_rgba[o + 1] = src[o + 1]; // G <- G | ||
| 1937 | out_rgba[o + 2] = src[o + 0]; // B <- R | ||
| 1938 | out_rgba[o + 3] = 0xFF; | ||
| 1939 | } | ||
| 1940 | } | ||
| 1615 | }; | 1941 | }; |
| 1616 | 1942 | ||
| 1617 | test "vulkan module imports" { | 1943 | test "vulkan module imports" { |