feat(vulkan): capture swapchain pixels
This commit is contained in:
@@ -56,9 +56,10 @@ pub use self::instance::{
|
|||||||
use self::instance::{cstring_vec, ensure_instance_extensions_available};
|
use self::instance::{cstring_vec, ensure_instance_extensions_available};
|
||||||
use self::resources::{
|
use self::resources::{
|
||||||
color_subresource_range, create_command_pool, create_depth_attachment, create_frame_sync,
|
color_subresource_range, create_command_pool, create_depth_attachment, create_frame_sync,
|
||||||
create_static_mesh_index_buffer, create_static_mesh_vertex_buffer, create_static_texture_image,
|
create_readback_buffer, create_static_mesh_index_buffer, create_static_mesh_vertex_buffer,
|
||||||
destroy_allocated_buffer, destroy_allocated_image, destroy_depth_attachment,
|
create_static_texture_image, destroy_allocated_buffer, destroy_allocated_image,
|
||||||
VulkanAllocatedBuffer, VulkanAllocatedImage, VulkanDepthAttachment, VulkanFrameSync,
|
destroy_depth_attachment, readback_buffer_bytes, VulkanAllocatedBuffer, VulkanAllocatedImage,
|
||||||
|
VulkanDepthAttachment, VulkanFrameSync,
|
||||||
};
|
};
|
||||||
pub use self::runtime::{
|
pub use self::runtime::{
|
||||||
create_vulkan_logical_device_probe, create_vulkan_logical_device_probe_for_request,
|
create_vulkan_logical_device_probe, create_vulkan_logical_device_probe_for_request,
|
||||||
|
|||||||
@@ -698,6 +698,49 @@ pub(super) fn destroy_allocated_buffer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Allocates a host-coherent transfer destination for a swapchain image copy.
|
||||||
|
pub(super) fn create_readback_buffer(
|
||||||
|
instance: &VulkanInstanceProbe,
|
||||||
|
device: &VulkanLogicalDeviceProbe,
|
||||||
|
byte_len: usize,
|
||||||
|
) -> Result<VulkanAllocatedBuffer, VulkanSmokeRendererError> {
|
||||||
|
create_host_visible_buffer(
|
||||||
|
instance,
|
||||||
|
device,
|
||||||
|
&vec![0; byte_len],
|
||||||
|
vk::BufferUsageFlags::TRANSFER_DST,
|
||||||
|
"vkCreateBuffer(readback)",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copies a completed host-coherent readback allocation into CPU-owned bytes.
|
||||||
|
pub(super) fn readback_buffer_bytes(
|
||||||
|
device: &VulkanLogicalDeviceProbe,
|
||||||
|
buffer: &VulkanAllocatedBuffer,
|
||||||
|
byte_len: usize,
|
||||||
|
) -> Result<Vec<u8>, VulkanSmokeRendererError> {
|
||||||
|
// SAFETY: The caller waits for device idle before mapping this host-visible allocation.
|
||||||
|
let mapped = unsafe {
|
||||||
|
device.device().map_memory(
|
||||||
|
buffer.memory,
|
||||||
|
0,
|
||||||
|
u64::try_from(byte_len).unwrap_or(u64::MAX),
|
||||||
|
vk::MemoryMapFlags::empty(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.map_err(|result| VulkanSmokeRendererError::VulkanOperation {
|
||||||
|
context: "vkMapMemory(readback)",
|
||||||
|
result,
|
||||||
|
})?;
|
||||||
|
let mut bytes = vec![0; byte_len];
|
||||||
|
// SAFETY: Both ranges contain exactly byte_len initialized bytes and do not overlap.
|
||||||
|
unsafe {
|
||||||
|
std::ptr::copy_nonoverlapping(mapped.cast::<u8>(), bytes.as_mut_ptr(), byte_len);
|
||||||
|
device.device().unmap_memory(buffer.memory);
|
||||||
|
}
|
||||||
|
Ok(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn color_subresource_range() -> vk::ImageSubresourceRange {
|
pub(super) fn color_subresource_range() -> vk::ImageSubresourceRange {
|
||||||
vk::ImageSubresourceRange::default()
|
vk::ImageSubresourceRange::default()
|
||||||
.aspect_mask(vk::ImageAspectFlags::COLOR)
|
.aspect_mask(vk::ImageAspectFlags::COLOR)
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ use super::{
|
|||||||
create_validation_messenger, create_vulkan_instance_probe,
|
create_validation_messenger, create_vulkan_instance_probe,
|
||||||
create_vulkan_logical_device_probe_for_request, create_vulkan_surface_probe,
|
create_vulkan_logical_device_probe_for_request, create_vulkan_surface_probe,
|
||||||
create_vulkan_swapchain_probe_for_extent, destroy_allocated_buffer, destroy_allocated_image,
|
create_vulkan_swapchain_probe_for_extent, destroy_allocated_buffer, destroy_allocated_image,
|
||||||
destroy_swapchain_resources, plan_vulkan_surface, VulkanAllocatedBuffer, VulkanInstanceConfig,
|
destroy_swapchain_resources, plan_vulkan_surface, readback_buffer_bytes, VulkanAllocatedBuffer,
|
||||||
VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanSmokeFrameOutcome, VulkanSmokeRenderer,
|
VulkanInstanceConfig, VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanSmokeFrameOutcome,
|
||||||
VulkanSmokeRendererCreateInfo, VulkanSmokeRendererError, VulkanSmokeRendererReport,
|
VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanSmokeRendererError,
|
||||||
VulkanSmokeShutdownReport, VulkanSurfaceProbe, VulkanSwapchainProbe, VulkanSwapchainResources,
|
VulkanSmokeRendererReport, VulkanSmokeShutdownReport, VulkanSurfaceProbe, VulkanSwapchainProbe,
|
||||||
VulkanValidationMessenger, VulkanValidationReport,
|
VulkanSwapchainResources, VulkanValidationMessenger, VulkanValidationReport,
|
||||||
};
|
};
|
||||||
use crate::policy::KHR_PORTABILITY_SUBSET_EXTENSION;
|
use crate::policy::KHR_PORTABILITY_SUBSET_EXTENSION;
|
||||||
use crate::shader_manifest::{triangle_shader_manifest, validate_shader_manifest};
|
use crate::shader_manifest::{triangle_shader_manifest, validate_shader_manifest};
|
||||||
@@ -242,6 +242,9 @@ impl VulkanSmokeRenderer {
|
|||||||
swapchain_extent: (0, 0),
|
swapchain_extent: (0, 0),
|
||||||
swapchain_image_count: 0,
|
swapchain_image_count: 0,
|
||||||
swapchain_image_usage: 0,
|
swapchain_image_usage: 0,
|
||||||
|
readback_copy_count: 0,
|
||||||
|
readback_byte_count: 0,
|
||||||
|
readback_fnv1a64: 0,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
renderer.rebuild_swapchain_resources(false)?;
|
renderer.rebuild_swapchain_resources(false)?;
|
||||||
@@ -270,6 +273,9 @@ impl VulkanSmokeRenderer {
|
|||||||
swapchain_extent: swapchain_ref.report.plan.extent,
|
swapchain_extent: swapchain_ref.report.plan.extent,
|
||||||
swapchain_image_count: swapchain_ref.report.image_count,
|
swapchain_image_count: swapchain_ref.report.image_count,
|
||||||
swapchain_image_usage: swapchain_ref.report.plan.image_usage,
|
swapchain_image_usage: swapchain_ref.report.plan.image_usage,
|
||||||
|
readback_copy_count: 0,
|
||||||
|
readback_byte_count: 0,
|
||||||
|
readback_fnv1a64: 0,
|
||||||
};
|
};
|
||||||
Ok(renderer)
|
Ok(renderer)
|
||||||
}
|
}
|
||||||
@@ -476,6 +482,9 @@ impl VulkanSmokeRenderer {
|
|||||||
context: "vkQueueSubmit",
|
context: "vkQueueSubmit",
|
||||||
result: error,
|
result: error,
|
||||||
})?;
|
})?;
|
||||||
|
if !self.resources_ref()?.readback_buffers.is_empty() {
|
||||||
|
self.report.readback_copy_count = self.report.readback_copy_count.saturating_add(1);
|
||||||
|
}
|
||||||
|
|
||||||
let present_wait = [render_finished];
|
let present_wait = [render_finished];
|
||||||
let swapchains = [self.swapchain_ref()?.swapchain()];
|
let swapchains = [self.swapchain_ref()?.swapchain()];
|
||||||
@@ -700,6 +709,64 @@ impl VulkanSmokeRenderer {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
device.device().cmd_end_render_pass(command_buffer);
|
device.device().cmd_end_render_pass(command_buffer);
|
||||||
|
if let (Some(image), Some(readback)) = (
|
||||||
|
resources.images.get(image_index),
|
||||||
|
resources.readback_buffers.get(image_index),
|
||||||
|
) {
|
||||||
|
let range = super::color_subresource_range();
|
||||||
|
let to_transfer = vk::ImageMemoryBarrier::default()
|
||||||
|
.old_layout(vk::ImageLayout::PRESENT_SRC_KHR)
|
||||||
|
.new_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL)
|
||||||
|
.src_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE)
|
||||||
|
.dst_access_mask(vk::AccessFlags::TRANSFER_READ)
|
||||||
|
.image(*image)
|
||||||
|
.subresource_range(range);
|
||||||
|
let back_to_present = vk::ImageMemoryBarrier::default()
|
||||||
|
.old_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL)
|
||||||
|
.new_layout(vk::ImageLayout::PRESENT_SRC_KHR)
|
||||||
|
.src_access_mask(vk::AccessFlags::TRANSFER_READ)
|
||||||
|
.dst_access_mask(vk::AccessFlags::empty())
|
||||||
|
.image(*image)
|
||||||
|
.subresource_range(range);
|
||||||
|
let region = vk::BufferImageCopy::default()
|
||||||
|
.image_subresource(
|
||||||
|
vk::ImageSubresourceLayers::default()
|
||||||
|
.aspect_mask(vk::ImageAspectFlags::COLOR)
|
||||||
|
.mip_level(0)
|
||||||
|
.base_array_layer(0)
|
||||||
|
.layer_count(1),
|
||||||
|
)
|
||||||
|
.image_extent(vk::Extent3D {
|
||||||
|
width: swapchain.report.plan.extent.0,
|
||||||
|
height: swapchain.report.plan.extent.1,
|
||||||
|
depth: 1,
|
||||||
|
});
|
||||||
|
device.device().cmd_pipeline_barrier(
|
||||||
|
command_buffer,
|
||||||
|
vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
|
||||||
|
vk::PipelineStageFlags::TRANSFER,
|
||||||
|
vk::DependencyFlags::empty(),
|
||||||
|
&[],
|
||||||
|
&[],
|
||||||
|
&[to_transfer],
|
||||||
|
);
|
||||||
|
device.device().cmd_copy_image_to_buffer(
|
||||||
|
command_buffer,
|
||||||
|
*image,
|
||||||
|
vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
|
||||||
|
readback.buffer,
|
||||||
|
&[region],
|
||||||
|
);
|
||||||
|
device.device().cmd_pipeline_barrier(
|
||||||
|
command_buffer,
|
||||||
|
vk::PipelineStageFlags::TRANSFER,
|
||||||
|
vk::PipelineStageFlags::BOTTOM_OF_PIPE,
|
||||||
|
vk::DependencyFlags::empty(),
|
||||||
|
&[],
|
||||||
|
&[],
|
||||||
|
&[back_to_present],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SAFETY: The render pass owns the attachment layout transitions for this clear-and-present path.
|
// SAFETY: The render pass owns the attachment layout transitions for this clear-and-present path.
|
||||||
@@ -766,7 +833,7 @@ impl VulkanSmokeRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn shutdown_inner(&mut self) -> Result<VulkanSmokeShutdownReport, VulkanSmokeRendererError> {
|
fn shutdown_inner(&mut self) -> Result<VulkanSmokeShutdownReport, VulkanSmokeRendererError> {
|
||||||
if let Some(device) = self.device.as_ref() {
|
let completed_readback = if let Some(device) = self.device.as_ref() {
|
||||||
// SAFETY: The logical device remains live until teardown finishes and idling prevents in-flight work from touching swapchain, buffers, sync objects or the command pool after destruction starts.
|
// SAFETY: The logical device remains live until teardown finishes and idling prevents in-flight work from touching swapchain, buffers, sync objects or the command pool after destruction starts.
|
||||||
unsafe { device.device().device_wait_idle() }.map_err(|error| {
|
unsafe { device.device().device_wait_idle() }.map_err(|error| {
|
||||||
VulkanSmokeRendererError::VulkanOperation {
|
VulkanSmokeRendererError::VulkanOperation {
|
||||||
@@ -774,6 +841,13 @@ impl VulkanSmokeRenderer {
|
|||||||
result: error,
|
result: error,
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
self.completed_readback(device)?
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if let Some((byte_count, hash)) = completed_readback {
|
||||||
|
self.report.readback_byte_count = byte_count;
|
||||||
|
self.report.readback_fnv1a64 = hash;
|
||||||
}
|
}
|
||||||
self.destroy_device_owned_resources();
|
self.destroy_device_owned_resources();
|
||||||
let validation = take_runtime_children_with_validation_snapshot(
|
let validation = take_runtime_children_with_validation_snapshot(
|
||||||
@@ -793,6 +867,40 @@ impl VulkanSmokeRenderer {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn completed_readback(
|
||||||
|
&self,
|
||||||
|
device: &VulkanLogicalDeviceProbe,
|
||||||
|
) -> Result<Option<(u64, u64)>, VulkanSmokeRendererError> {
|
||||||
|
let Some(resources) = self.swapchain_resources.as_ref() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let byte_len = usize::try_from(self.report.swapchain_extent.0)
|
||||||
|
.ok()
|
||||||
|
.and_then(|width| {
|
||||||
|
usize::try_from(self.report.swapchain_extent.1)
|
||||||
|
.ok()
|
||||||
|
.and_then(|height| width.checked_mul(height))
|
||||||
|
})
|
||||||
|
.and_then(|pixels| pixels.checked_mul(4))
|
||||||
|
.ok_or(VulkanSmokeRendererError::InvalidStaticMesh {
|
||||||
|
context: "completed readback byte length",
|
||||||
|
})?;
|
||||||
|
if resources.readback_buffers.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let mut hash = 0xcbf2_9ce4_8422_2325_u64;
|
||||||
|
let mut byte_count = 0_u64;
|
||||||
|
for buffer in &resources.readback_buffers {
|
||||||
|
let bytes = readback_buffer_bytes(device, buffer, byte_len)?;
|
||||||
|
for byte in bytes {
|
||||||
|
hash ^= u64::from(byte);
|
||||||
|
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
|
||||||
|
}
|
||||||
|
byte_count = byte_count.saturating_add(u64::try_from(byte_len).unwrap_or(u64::MAX));
|
||||||
|
}
|
||||||
|
Ok(Some((byte_count, hash)))
|
||||||
|
}
|
||||||
|
|
||||||
fn teardown(&mut self) {
|
fn teardown(&mut self) {
|
||||||
if let Some(device) = self.device.as_ref() {
|
if let Some(device) = self.device.as_ref() {
|
||||||
// SAFETY: The logical device remains live until teardown finishes and idling prevents in-flight work from touching swapchain, buffers, sync objects or the command pool after destruction starts.
|
// SAFETY: The logical device remains live until teardown finishes and idling prevents in-flight work from touching swapchain, buffers, sync objects or the command pool after destruction starts.
|
||||||
|
|||||||
@@ -459,6 +459,12 @@ pub struct VulkanSmokeRendererReport {
|
|||||||
pub swapchain_image_count: u32,
|
pub swapchain_image_count: u32,
|
||||||
/// Current swapchain image-usage flags as raw Vulkan bits.
|
/// Current swapchain image-usage flags as raw Vulkan bits.
|
||||||
pub swapchain_image_usage: u32,
|
pub swapchain_image_usage: u32,
|
||||||
|
/// Number of submitted swapchain image-to-buffer copy commands.
|
||||||
|
pub readback_copy_count: u64,
|
||||||
|
/// Byte length of the final completed readback artifact.
|
||||||
|
pub readback_byte_count: u64,
|
||||||
|
/// Stable FNV-1a hash of the final completed readback artifact.
|
||||||
|
pub readback_fnv1a64: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Measured validation counters from the live smoke loop.
|
/// Measured validation counters from the live smoke loop.
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ use super::{
|
|||||||
|
|
||||||
pub(super) struct VulkanSwapchainResources {
|
pub(super) struct VulkanSwapchainResources {
|
||||||
pub(super) image_views: Vec<vk::ImageView>,
|
pub(super) image_views: Vec<vk::ImageView>,
|
||||||
|
pub(super) images: Vec<vk::Image>,
|
||||||
|
pub(super) readback_buffers: Vec<VulkanAllocatedBuffer>,
|
||||||
pub(super) depth_attachment: VulkanDepthAttachment,
|
pub(super) depth_attachment: VulkanDepthAttachment,
|
||||||
pub(super) render_pass: vk::RenderPass,
|
pub(super) render_pass: vk::RenderPass,
|
||||||
pub(super) pipeline_layout: vk::PipelineLayout,
|
pub(super) pipeline_layout: vk::PipelineLayout,
|
||||||
@@ -31,6 +33,7 @@ pub(super) struct VulkanSwapchainResources {
|
|||||||
|
|
||||||
struct PartialSwapchainResources {
|
struct PartialSwapchainResources {
|
||||||
image_views: Vec<vk::ImageView>,
|
image_views: Vec<vk::ImageView>,
|
||||||
|
readback_buffers: Vec<VulkanAllocatedBuffer>,
|
||||||
depth_attachment: Option<VulkanDepthAttachment>,
|
depth_attachment: Option<VulkanDepthAttachment>,
|
||||||
render_pass: Option<vk::RenderPass>,
|
render_pass: Option<vk::RenderPass>,
|
||||||
pipeline_layout: Option<vk::PipelineLayout>,
|
pipeline_layout: Option<vk::PipelineLayout>,
|
||||||
@@ -71,6 +74,7 @@ pub(super) fn create_swapchain_resources(
|
|||||||
&images,
|
&images,
|
||||||
swapchain.report.plan.format.format,
|
swapchain.report.plan.format.format,
|
||||||
)?,
|
)?,
|
||||||
|
readback_buffers: Vec::new(),
|
||||||
depth_attachment: None,
|
depth_attachment: None,
|
||||||
render_pass: None,
|
render_pass: None,
|
||||||
pipeline_layout: None,
|
pipeline_layout: None,
|
||||||
@@ -127,6 +131,30 @@ pub(super) fn create_swapchain_resources(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
partial.framebuffers = framebuffers;
|
partial.framebuffers = framebuffers;
|
||||||
|
if vk::ImageUsageFlags::from_raw(swapchain.report.plan.image_usage)
|
||||||
|
.contains(vk::ImageUsageFlags::TRANSFER_SRC)
|
||||||
|
{
|
||||||
|
let byte_len = usize::try_from(swapchain.report.plan.extent.0)
|
||||||
|
.ok()
|
||||||
|
.and_then(|width| {
|
||||||
|
usize::try_from(swapchain.report.plan.extent.1)
|
||||||
|
.ok()
|
||||||
|
.and_then(|height| width.checked_mul(height))
|
||||||
|
})
|
||||||
|
.and_then(|pixels| pixels.checked_mul(4))
|
||||||
|
.ok_or(VulkanSmokeRendererError::InvalidStaticMesh {
|
||||||
|
context: "swapchain readback byte length",
|
||||||
|
})?;
|
||||||
|
for _ in &images {
|
||||||
|
match super::create_readback_buffer(instance, device, byte_len) {
|
||||||
|
Ok(buffer) => partial.readback_buffers.push(buffer),
|
||||||
|
Err(error) => {
|
||||||
|
destroy_partial_swapchain_resources(device, command_pool, partial);
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
reset_reusable_command_pool(device, command_pool, reuse_command_pool)?;
|
reset_reusable_command_pool(device, command_pool, reuse_command_pool)?;
|
||||||
let command_buffers = match allocate_command_buffers(
|
let command_buffers = match allocate_command_buffers(
|
||||||
device,
|
device,
|
||||||
@@ -150,6 +178,8 @@ pub(super) fn create_swapchain_resources(
|
|||||||
})?;
|
})?;
|
||||||
Ok(VulkanSwapchainResources {
|
Ok(VulkanSwapchainResources {
|
||||||
image_views: partial.image_views,
|
image_views: partial.image_views,
|
||||||
|
images,
|
||||||
|
readback_buffers: partial.readback_buffers,
|
||||||
depth_attachment,
|
depth_attachment,
|
||||||
render_pass,
|
render_pass,
|
||||||
pipeline_layout,
|
pipeline_layout,
|
||||||
@@ -795,6 +825,10 @@ pub(super) fn destroy_swapchain_resources(
|
|||||||
for image_view in resources.image_views {
|
for image_view in resources.image_views {
|
||||||
device.device().destroy_image_view(image_view, None);
|
device.device().destroy_image_view(image_view, None);
|
||||||
}
|
}
|
||||||
|
for buffer in resources.readback_buffers {
|
||||||
|
device.device().destroy_buffer(buffer.buffer, None);
|
||||||
|
device.device().free_memory(buffer.memory, None);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -843,5 +877,9 @@ fn destroy_partial_swapchain_resources(
|
|||||||
for image_view in partial.image_views {
|
for image_view in partial.image_views {
|
||||||
device.device().destroy_image_view(image_view, None);
|
device.device().destroy_image_view(image_view, None);
|
||||||
}
|
}
|
||||||
|
for buffer in partial.readback_buffers {
|
||||||
|
device.device().destroy_buffer(buffer.buffer, None);
|
||||||
|
device.device().free_memory(buffer.memory, None);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -781,6 +781,15 @@ impl SmokeApp {
|
|||||||
vulkan_swapchain_image_usage: renderer
|
vulkan_swapchain_image_usage: renderer
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(0, |snapshot| snapshot.report.swapchain_image_usage),
|
.map_or(0, |snapshot| snapshot.report.swapchain_image_usage),
|
||||||
|
vulkan_readback_copy_count: renderer
|
||||||
|
.as_ref()
|
||||||
|
.map_or(0, |snapshot| snapshot.report.readback_copy_count),
|
||||||
|
vulkan_readback_byte_count: renderer
|
||||||
|
.as_ref()
|
||||||
|
.map_or(0, |snapshot| snapshot.report.readback_byte_count),
|
||||||
|
vulkan_readback_fnv1a64: renderer
|
||||||
|
.as_ref()
|
||||||
|
.map_or(0, |snapshot| snapshot.report.readback_fnv1a64),
|
||||||
vulkan_portability_enumeration: renderer
|
vulkan_portability_enumeration: renderer
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|snapshot| snapshot.report.portability_enumeration),
|
.is_some_and(|snapshot| snapshot.report.portability_enumeration),
|
||||||
@@ -984,6 +993,9 @@ fn render_timeout_failure_report(
|
|||||||
vulkan_swapchain_height: 0,
|
vulkan_swapchain_height: 0,
|
||||||
vulkan_swapchain_image_count: 0,
|
vulkan_swapchain_image_count: 0,
|
||||||
vulkan_swapchain_image_usage: 0,
|
vulkan_swapchain_image_usage: 0,
|
||||||
|
vulkan_readback_copy_count: 0,
|
||||||
|
vulkan_readback_byte_count: 0,
|
||||||
|
vulkan_readback_fnv1a64: 0,
|
||||||
vulkan_portability_enumeration: false,
|
vulkan_portability_enumeration: false,
|
||||||
vulkan_portability_subset_enabled: false,
|
vulkan_portability_subset_enabled: false,
|
||||||
};
|
};
|
||||||
@@ -1205,6 +1217,9 @@ struct SmokeReport<'a> {
|
|||||||
vulkan_swapchain_height: u32,
|
vulkan_swapchain_height: u32,
|
||||||
vulkan_swapchain_image_count: u32,
|
vulkan_swapchain_image_count: u32,
|
||||||
vulkan_swapchain_image_usage: u32,
|
vulkan_swapchain_image_usage: u32,
|
||||||
|
vulkan_readback_copy_count: u64,
|
||||||
|
vulkan_readback_byte_count: u64,
|
||||||
|
vulkan_readback_fnv1a64: u64,
|
||||||
vulkan_portability_enumeration: bool,
|
vulkan_portability_enumeration: bool,
|
||||||
vulkan_portability_subset_enabled: bool,
|
vulkan_portability_subset_enabled: bool,
|
||||||
}
|
}
|
||||||
@@ -1709,6 +1724,9 @@ mod tests {
|
|||||||
vulkan_swapchain_height: 540,
|
vulkan_swapchain_height: 540,
|
||||||
vulkan_swapchain_image_count: 3,
|
vulkan_swapchain_image_count: 3,
|
||||||
vulkan_swapchain_image_usage: 17,
|
vulkan_swapchain_image_usage: 17,
|
||||||
|
vulkan_readback_copy_count: 300,
|
||||||
|
vulkan_readback_byte_count: 4_147_200,
|
||||||
|
vulkan_readback_fnv1a64: 0x1234_5678_90ab_cdef,
|
||||||
vulkan_portability_enumeration: false,
|
vulkan_portability_enumeration: false,
|
||||||
vulkan_portability_subset_enabled: false,
|
vulkan_portability_subset_enabled: false,
|
||||||
})
|
})
|
||||||
@@ -1721,6 +1739,8 @@ mod tests {
|
|||||||
assert!(json.contains("\"mesh_source\": \"original-msh\""));
|
assert!(json.contains("\"mesh_source\": \"original-msh\""));
|
||||||
assert!(json.contains("\"mesh_draw_count\": 1"));
|
assert!(json.contains("\"mesh_draw_count\": 1"));
|
||||||
assert!(json.contains("\"texture_source\": \"original-texm\""));
|
assert!(json.contains("\"texture_source\": \"original-texm\""));
|
||||||
|
assert!(json.contains("\"vulkan_readback_copy_count\": 300"));
|
||||||
|
assert!(json.contains("\"vulkan_readback_byte_count\": 4147200"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -875,6 +875,26 @@ events, 2 swapchain recreations, Vulkan validation warnings/errors `0/0`.
|
|||||||
Следующий шаг должен выполнить явный image-to-buffer copy и сравнить полученные
|
Следующий шаг должен выполнить явный image-to-buffer copy и сравнить полученные
|
||||||
bytes с зафиксированным reference capture.
|
bytes с зафиксированным reference capture.
|
||||||
|
|
||||||
|
### First synchronized Vulkan pixel-readback artifact
|
||||||
|
|
||||||
|
Stage 3 static viewer теперь выполняет фактический readback для surface с
|
||||||
|
`TRANSFER_SRC`: на каждый swapchain image создаётся host-visible coherent
|
||||||
|
`TRANSFER_DST` buffer. После render pass command buffer переводит image из
|
||||||
|
`PRESENT_SRC_KHR` в `TRANSFER_SRC_OPTIMAL`, выполняет `vkCmdCopyImageToBuffer`
|
||||||
|
и возвращает image в `PRESENT_SRC_KHR`. CPU отображает memory только после
|
||||||
|
`vkDeviceWaitIdle` during shutdown; это исключает чтение GPU work in flight.
|
||||||
|
Smoke JSON фиксирует число записанных copy-команд, final byte count и FNV-1a-64
|
||||||
|
hash всех current-swapchain readback buffers, не сохраняя игровые pixels в
|
||||||
|
репозитории.
|
||||||
|
|
||||||
|
На GOG `MTCHECK.MSH`/`DEFAULT.0` AMD Radeon Pro WX 3200 Series выполнил 300
|
||||||
|
copy-команд, final artifact 4,147,200 bytes и hash `2184179010340020629` при
|
||||||
|
validation warnings/errors `0/0`. Повторный идентичный запуск дал тот же
|
||||||
|
размер и hash. Это доказывает Vulkan copy/readback path только для нашего
|
||||||
|
static viewer. Он ещё не захватывает original DirectDraw frame, не задаёт
|
||||||
|
fixed original camera и не сравнивает два изображения, поэтому pixel-parity
|
||||||
|
acceptance остаётся blocked.
|
||||||
|
|
||||||
`Land.msh` использует отдельный geometry-only bridge: validated `TerrainFace28`
|
`Land.msh` использует отдельный geometry-only bridge: validated `TerrainFace28`
|
||||||
сохраняет source triangle order, а его positions и packed UV0 попадают в тот же
|
сохраняет source triangle order, а его positions и packed UV0 попадают в тот же
|
||||||
static vertex/index upload path. Для текущего диагностического viewer XZ bounds
|
static vertex/index upload path. Для текущего диагностического viewer XZ bounds
|
||||||
|
|||||||
Reference in New Issue
Block a user