fix(render): export final Vulkan readback image
This commit is contained in:
@@ -235,6 +235,7 @@ impl VulkanSmokeRenderer {
|
|||||||
frame_sync: Vec::new(),
|
frame_sync: Vec::new(),
|
||||||
images_in_flight: Vec::new(),
|
images_in_flight: Vec::new(),
|
||||||
current_frame: 0,
|
current_frame: 0,
|
||||||
|
last_readback_image_index: None,
|
||||||
depth_request: create_info.render_request.depth,
|
depth_request: create_info.render_request.depth,
|
||||||
pending_extent: None,
|
pending_extent: None,
|
||||||
swapchain_recreate_count: 0,
|
swapchain_recreate_count: 0,
|
||||||
@@ -519,6 +520,7 @@ impl VulkanSmokeRenderer {
|
|||||||
})?;
|
})?;
|
||||||
if !self.resources_ref()?.readback_buffers.is_empty() {
|
if !self.resources_ref()?.readback_buffers.is_empty() {
|
||||||
self.report.readback_copy_count = self.report.readback_copy_count.saturating_add(1);
|
self.report.readback_copy_count = self.report.readback_copy_count.saturating_add(1);
|
||||||
|
self.last_readback_image_index = Some(image_index_usize);
|
||||||
}
|
}
|
||||||
|
|
||||||
let present_wait = [render_finished];
|
let present_wait = [render_finished];
|
||||||
@@ -621,6 +623,7 @@ impl VulkanSmokeRenderer {
|
|||||||
let resources = resources.commit();
|
let resources = resources.commit();
|
||||||
self.images_in_flight = vec![vk::Fence::null(); resources.image_views.len()];
|
self.images_in_flight = vec![vk::Fence::null(); resources.image_views.len()];
|
||||||
self.frame_sync = frame_sync;
|
self.frame_sync = frame_sync;
|
||||||
|
self.last_readback_image_index = None;
|
||||||
self.report.swapchain_extent = swapchain_extent;
|
self.report.swapchain_extent = swapchain_extent;
|
||||||
self.report.swapchain_image_count = swapchain_image_count;
|
self.report.swapchain_image_count = swapchain_image_count;
|
||||||
self.report.swapchain_image_format = self.swapchain_ref()?.report.plan.format.format;
|
self.report.swapchain_image_format = self.swapchain_ref()?.report.plan.format.format;
|
||||||
@@ -941,13 +944,19 @@ impl VulkanSmokeRenderer {
|
|||||||
if resources.readback_buffers.is_empty() {
|
if resources.readback_buffers.is_empty() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let mut artifact =
|
let Some(image_index) = completed_readback_buffer_index(
|
||||||
Vec::with_capacity(byte_len.saturating_mul(resources.readback_buffers.len()));
|
self.last_readback_image_index,
|
||||||
for buffer in &resources.readback_buffers {
|
resources.readback_buffers.len(),
|
||||||
let bytes = readback_buffer_bytes(device, buffer, byte_len)?;
|
)?
|
||||||
artifact.extend_from_slice(&bytes);
|
else {
|
||||||
}
|
return Ok(None);
|
||||||
Ok(Some(artifact))
|
};
|
||||||
|
let buffer = resources.readback_buffers.get(image_index).ok_or(
|
||||||
|
VulkanSmokeRendererError::InvariantViolation {
|
||||||
|
context: "last readback image index",
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
Ok(Some(readback_buffer_bytes(device, buffer, byte_len)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn teardown(&mut self) {
|
fn teardown(&mut self) {
|
||||||
@@ -968,6 +977,19 @@ impl VulkanSmokeRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn completed_readback_buffer_index(
|
||||||
|
last_image_index: Option<usize>,
|
||||||
|
buffer_count: usize,
|
||||||
|
) -> Result<Option<usize>, VulkanSmokeRendererError> {
|
||||||
|
match last_image_index {
|
||||||
|
None => Ok(None),
|
||||||
|
Some(index) if index < buffer_count => Ok(Some(index)),
|
||||||
|
Some(_) => Err(VulkanSmokeRendererError::InvariantViolation {
|
||||||
|
context: "last readback image index",
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn matrix_bytes(matrix: [f32; 16]) -> [u8; 64] {
|
fn matrix_bytes(matrix: [f32; 16]) -> [u8; 64] {
|
||||||
let mut bytes = [0; 64];
|
let mut bytes = [0; 64];
|
||||||
for (index, value) in matrix.into_iter().enumerate() {
|
for (index, value) in matrix.into_iter().enumerate() {
|
||||||
@@ -991,8 +1013,8 @@ impl Drop for VulkanSmokeRenderer {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
take_runtime_children_with_validation_snapshot, take_runtime_owners_in_dependency_order,
|
completed_readback_buffer_index, take_runtime_children_with_validation_snapshot,
|
||||||
RollbackOnDrop,
|
take_runtime_owners_in_dependency_order, RollbackOnDrop,
|
||||||
};
|
};
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
@@ -1075,6 +1097,13 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn completed_readback_selects_only_the_last_submitted_image() {
|
||||||
|
assert_eq!(completed_readback_buffer_index(None, 2), Ok(None));
|
||||||
|
assert_eq!(completed_readback_buffer_index(Some(1), 2), Ok(Some(1)));
|
||||||
|
assert!(completed_readback_buffer_index(Some(2), 2).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn runtime_owners_drop_remaining_children_after_partial_init_failures() {
|
fn runtime_owners_drop_remaining_children_after_partial_init_failures() {
|
||||||
let cases = [
|
let cases = [
|
||||||
|
|||||||
@@ -603,7 +603,7 @@ pub struct VulkanValidationReport {
|
|||||||
pub vuids: Vec<String>,
|
pub vuids: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// CPU-owned copy of the final completed swapchain readback buffers.
|
/// CPU-owned copy of the final completed swapchain image readback.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct VulkanReadbackArtifact {
|
pub struct VulkanReadbackArtifact {
|
||||||
/// Vulkan swapchain format as a raw enum value.
|
/// Vulkan swapchain format as a raw enum value.
|
||||||
@@ -612,7 +612,8 @@ pub struct VulkanReadbackArtifact {
|
|||||||
pub width: u32,
|
pub width: u32,
|
||||||
/// Height of each image in pixels.
|
/// Height of each image in pixels.
|
||||||
pub height: u32,
|
pub height: u32,
|
||||||
/// Concatenated raw four-byte-per-pixel images in swapchain order.
|
/// One raw four-byte-per-pixel image: the last successfully submitted
|
||||||
|
/// swapchain image before synchronized teardown.
|
||||||
pub bytes: Vec<u8>,
|
pub bytes: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -735,6 +736,10 @@ pub struct VulkanSmokeRenderer {
|
|||||||
pub(super) frame_sync: Vec<VulkanFrameSync>,
|
pub(super) frame_sync: Vec<VulkanFrameSync>,
|
||||||
pub(super) images_in_flight: Vec<vk::Fence>,
|
pub(super) images_in_flight: Vec<vk::Fence>,
|
||||||
pub(super) current_frame: usize,
|
pub(super) current_frame: usize,
|
||||||
|
/// Last swapchain image whose color attachment was copied after a
|
||||||
|
/// successful graphics submission. Reset whenever swapchain resources are
|
||||||
|
/// replaced so a teardown never reads a buffer from an old swapchain.
|
||||||
|
pub(super) last_readback_image_index: Option<usize>,
|
||||||
pub(super) depth_request: fparkan_platform::DepthStencilSupport,
|
pub(super) depth_request: fparkan_platform::DepthStencilSupport,
|
||||||
pub(super) pending_extent: Option<(u32, u32)>,
|
pub(super) pending_extent: Option<(u32, u32)>,
|
||||||
pub(super) swapchain_recreate_count: u32,
|
pub(super) swapchain_recreate_count: u32,
|
||||||
|
|||||||
+19
-9
@@ -877,6 +877,14 @@ bytes с зафиксированным reference capture.
|
|||||||
|
|
||||||
### First synchronized Vulkan pixel-readback artifact
|
### First synchronized Vulkan pixel-readback artifact
|
||||||
|
|
||||||
|
The current artifact contract is one final swapchain image, not a
|
||||||
|
concatenation of every swapchain allocation. After the final successful
|
||||||
|
graphics submission, the renderer retains that image index; synchronized
|
||||||
|
teardown reads only its matching buffer. The raw payload is therefore exactly
|
||||||
|
`width * height * 4` bytes for the current format-50 path. Historical
|
||||||
|
multi-image byte counts below describe the superseded concatenation behavior
|
||||||
|
and are not comparable to this final-image contract.
|
||||||
|
|
||||||
Stage 3 static viewer теперь выполняет фактический readback для surface с
|
Stage 3 static viewer теперь выполняет фактический readback для surface с
|
||||||
`TRANSFER_SRC`: на каждый swapchain image создаётся host-visible coherent
|
`TRANSFER_SRC`: на каждый swapchain image создаётся host-visible coherent
|
||||||
`TRANSFER_DST` buffer. После render pass command buffer переводит image из
|
`TRANSFER_DST` buffer. После render pass command buffer переводит image из
|
||||||
@@ -895,8 +903,11 @@ static viewer. Он ещё не захватывает original DirectDraw frame
|
|||||||
fixed original camera и не сравнивает два изображения, поэтому pixel-parity
|
fixed original camera и не сравнивает два изображения, поэтому pixel-parity
|
||||||
acceptance остаётся blocked.
|
acceptance остаётся blocked.
|
||||||
|
|
||||||
Smoke также сохраняет raw artifact рядом с JSON: `<report-stem>.readback-vkformat-<raw>.raw`.
|
The following paragraph records the superseded multi-image export only as
|
||||||
Это concatenated current-swapchain images in Vulkan order, каждый с dimensions
|
historical bootstrap evidence; it is not the current artifact contract.
|
||||||
|
|
||||||
|
Smoke также сохранял raw artifact рядом с JSON: `<report-stem>.readback-vkformat-<raw>.raw`.
|
||||||
|
Это были concatenated current-swapchain images in Vulkan order, каждый с dimensions
|
||||||
из JSON и четырьмя bytes per pixel; format намеренно указан в имени файла,
|
из JSON и четырьмя bytes per pixel; format намеренно указан в имени файла,
|
||||||
потому что bytes не перекодируются; JSON содержит actual raw enum. GOG selected `50` and produced a 4,147,200-byte file
|
потому что bytes не перекодируются; JSON содержит actual raw enum. GOG selected `50` and produced a 4,147,200-byte file
|
||||||
for two 960x540 images. Артефакт остаётся локальным output и не попадает в Git.
|
for two 960x540 images. Артефакт остаётся локальным output и не попадает в Git.
|
||||||
@@ -1536,13 +1547,12 @@ not yet pixel parity, because the static bridge still lacks the full runtime
|
|||||||
camera-selection timing, terrain composition, culling, lighting, UI and FX.
|
camera-selection timing, terrain composition, culling, lighting, UI and FX.
|
||||||
|
|
||||||
For visual regression work, `fparkan-game --backend static-vulkan` also accepts
|
For visual regression work, `fparkan-game --backend static-vulkan` also accepts
|
||||||
`--readback-out <path>`. It writes the final synchronized Vulkan readback bytes
|
`--readback-out <path>`. It writes the final synchronized Vulkan swapchain
|
||||||
only after the renderer has completed its normal teardown evidence; the JSON
|
image only after the renderer has completed its normal teardown evidence; the
|
||||||
report records the raw Vulkan format, byte count, hash, and requested path. A
|
JSON report records its raw Vulkan format, byte count, hash, and requested
|
||||||
three-frame diagnostic AutoDemo run wrote 7,372,800 bytes at format `50`
|
path. At 1280x720, format `50` (`VK_FORMAT_B8G8R8A8_UNORM`) produces exactly
|
||||||
(`VK_FORMAT_B8G8R8A8_UNORM`) with the same frame hash reported by the renderer.
|
3,686,400 bytes. The artifact is a Vulkan-side comparison input, not an
|
||||||
The artifact is a Vulkan-side comparison input, not an asserted original-frame
|
asserted original-frame image.
|
||||||
image.
|
|
||||||
|
|
||||||
The first bounded launch showed that repeated fingerprinting, rather than
|
The first bounded launch showed that repeated fingerprinting, rather than
|
||||||
Vulkan initialization, was the load-path bottleneck: each new MAT0/TEXM request
|
Vulkan initialization, was the load-path bottleneck: each new MAT0/TEXM request
|
||||||
|
|||||||
Reference in New Issue
Block a user