feat(vulkan): export readback artifacts
Docs Deploy / Build and Deploy MkDocs (push) Successful in 36s
Test / Lint (push) Failing after 2m4s
Test / Test (push) Skipped
Test / Render parity (push) Skipped

This commit is contained in:
2026-07-18 09:08:07 +04:00
parent f9a13298c0
commit b24594c6a2
5 changed files with 87 additions and 26 deletions
+5 -5
View File
@@ -66,11 +66,11 @@ pub use self::runtime::{
VulkanLogicalDeviceError, VulkanLogicalDeviceProbe, VulkanLogicalDeviceReport, VulkanLogicalDeviceError, VulkanLogicalDeviceProbe, VulkanLogicalDeviceReport,
}; };
pub use self::smoke_types::{ pub use self::smoke_types::{
VulkanSmokeBootstrapProgress, VulkanSmokeBootstrapSnapshot, VulkanSmokeFrameOutcome, VulkanReadbackArtifact, VulkanSmokeBootstrapProgress, VulkanSmokeBootstrapSnapshot,
VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanSmokeRendererError, VulkanSmokeFrameOutcome, VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo,
VulkanSmokeRendererReport, VulkanSmokeShutdownReport, VulkanStaticDrawRange, VulkanSmokeRendererError, VulkanSmokeRendererReport, VulkanSmokeShutdownReport,
VulkanStaticMaterial, VulkanStaticMesh, VulkanStaticTexture, VulkanStaticVertex, VulkanStaticDrawRange, VulkanStaticMaterial, VulkanStaticMesh, VulkanStaticTexture,
VulkanValidationReport, VulkanStaticVertex, VulkanValidationReport,
}; };
#[cfg(test)] #[cfg(test)]
use self::surface::extension_name; use self::surface::extension_name;
+29 -17
View File
@@ -10,10 +10,11 @@ use super::{
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, readback_buffer_bytes, VulkanAllocatedBuffer, destroy_swapchain_resources, plan_vulkan_surface, readback_buffer_bytes, VulkanAllocatedBuffer,
VulkanInstanceConfig, VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanSmokeFrameOutcome, VulkanInstanceConfig, VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanReadbackArtifact,
VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanSmokeRendererError, VulkanSmokeFrameOutcome, VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo,
VulkanSmokeRendererReport, VulkanSmokeShutdownReport, VulkanSurfaceProbe, VulkanSwapchainProbe, VulkanSmokeRendererError, VulkanSmokeRendererReport, VulkanSmokeShutdownReport,
VulkanSwapchainResources, VulkanValidationMessenger, VulkanValidationReport, VulkanSurfaceProbe, VulkanSwapchainProbe, 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};
@@ -845,10 +846,18 @@ impl VulkanSmokeRenderer {
} else { } else {
None None
}; };
if let Some((byte_count, hash)) = completed_readback { let readback_artifact = completed_readback.map(|bytes| {
self.report.readback_byte_count = byte_count; self.report.readback_byte_count = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
self.report.readback_fnv1a64 = hash; self.report.readback_fnv1a64 = fnv1a64(&bytes);
} VulkanReadbackArtifact {
format: self
.swapchain_ref()
.map_or(0, |swapchain| swapchain.report.plan.format.format),
width: self.report.swapchain_extent.0,
height: self.report.swapchain_extent.1,
bytes,
}
});
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(
&mut self.surface, &mut self.surface,
@@ -862,6 +871,7 @@ impl VulkanSmokeRenderer {
self.instance.take(); self.instance.take();
Ok(VulkanSmokeShutdownReport { Ok(VulkanSmokeShutdownReport {
renderer_report: self.report.clone(), renderer_report: self.report.clone(),
readback_artifact,
swapchain_recreate_count: self.swapchain_recreate_count, swapchain_recreate_count: self.swapchain_recreate_count,
validation, validation,
}) })
@@ -870,7 +880,7 @@ impl VulkanSmokeRenderer {
fn completed_readback( fn completed_readback(
&self, &self,
device: &VulkanLogicalDeviceProbe, device: &VulkanLogicalDeviceProbe,
) -> Result<Option<(u64, u64)>, VulkanSmokeRendererError> { ) -> Result<Option<Vec<u8>>, VulkanSmokeRendererError> {
let Some(resources) = self.swapchain_resources.as_ref() else { let Some(resources) = self.swapchain_resources.as_ref() else {
return Ok(None); return Ok(None);
}; };
@@ -888,17 +898,13 @@ impl VulkanSmokeRenderer {
if resources.readback_buffers.is_empty() { if resources.readback_buffers.is_empty() {
return Ok(None); return Ok(None);
} }
let mut hash = 0xcbf2_9ce4_8422_2325_u64; let mut artifact =
let mut byte_count = 0_u64; Vec::with_capacity(byte_len.saturating_mul(resources.readback_buffers.len()));
for buffer in &resources.readback_buffers { for buffer in &resources.readback_buffers {
let bytes = readback_buffer_bytes(device, buffer, byte_len)?; let bytes = readback_buffer_bytes(device, buffer, byte_len)?;
for byte in bytes { artifact.extend_from_slice(&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))) Ok(Some(artifact))
} }
fn teardown(&mut self) { fn teardown(&mut self) {
@@ -919,6 +925,12 @@ impl VulkanSmokeRenderer {
} }
} }
fn fnv1a64(bytes: &[u8]) -> u64 {
bytes.iter().fold(0xcbf2_9ce4_8422_2325_u64, |hash, byte| {
(hash ^ u64::from(*byte)).wrapping_mul(0x0000_0100_0000_01b3)
})
}
impl Drop for VulkanSmokeRenderer { impl Drop for VulkanSmokeRenderer {
fn drop(&mut self) { fn drop(&mut self) {
self.teardown(); self.teardown();
@@ -478,11 +478,26 @@ pub struct VulkanValidationReport {
pub vuids: Vec<String>, pub vuids: Vec<String>,
} }
/// CPU-owned copy of the final completed swapchain readback buffers.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VulkanReadbackArtifact {
/// Vulkan swapchain format as a raw enum value.
pub format: i32,
/// Width of each image in pixels.
pub width: u32,
/// Height of each image in pixels.
pub height: u32,
/// Concatenated raw four-byte-per-pixel images in swapchain order.
pub bytes: Vec<u8>,
}
/// Final smoke renderer shutdown evidence captured after explicit teardown. /// Final smoke renderer shutdown evidence captured after explicit teardown.
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct VulkanSmokeShutdownReport { pub struct VulkanSmokeShutdownReport {
/// Stable renderer bootstrap and swapchain report. /// Stable renderer bootstrap and swapchain report.
pub renderer_report: VulkanSmokeRendererReport, pub renderer_report: VulkanSmokeRendererReport,
/// Optional raw GPU readback retained after synchronization and before teardown.
pub readback_artifact: Option<VulkanReadbackArtifact>,
/// Measured swapchain recreation count for the completed smoke loop. /// Measured swapchain recreation count for the completed smoke loop.
pub swapchain_recreate_count: u32, pub swapchain_recreate_count: u32,
/// Final validation snapshot captured before the debug messenger is destroyed. /// Final validation snapshot captured before the debug messenger is destroyed.
+32 -4
View File
@@ -14,10 +14,10 @@
use fparkan_platform::RenderRequest; use fparkan_platform::RenderRequest;
use fparkan_platform_winit::{window_native_handles, WinitWindowPlan}; use fparkan_platform_winit::{window_native_handles, WinitWindowPlan};
use fparkan_render_vulkan::{ use fparkan_render_vulkan::{
project_land_msh_to_static_mesh, project_msh_to_static_mesh, VulkanSmokeBootstrapProgress, project_land_msh_to_static_mesh, project_msh_to_static_mesh, VulkanReadbackArtifact,
VulkanSmokeFrameOutcome, VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanSmokeBootstrapProgress, VulkanSmokeFrameOutcome, VulkanSmokeRenderer,
VulkanSmokeRendererReport, VulkanSmokeShutdownReport, VulkanStaticMaterial, VulkanStaticMesh, VulkanSmokeRendererCreateInfo, VulkanSmokeRendererReport, VulkanSmokeShutdownReport,
VulkanStaticTexture, VulkanValidationReport, VulkanStaticMaterial, VulkanStaticMesh, VulkanStaticTexture, VulkanValidationReport,
}; };
use serde::Serialize; use serde::Serialize;
use std::path::PathBuf; use std::path::PathBuf;
@@ -557,6 +557,7 @@ fn drop_renderer_before_window<Renderer, WindowLike>(
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct RendererSnapshot { struct RendererSnapshot {
report: VulkanSmokeRendererReport, report: VulkanSmokeRendererReport,
readback_artifact: Option<VulkanReadbackArtifact>,
swapchain_recreate_count: u32, swapchain_recreate_count: u32,
validation: VulkanValidationReport, validation: VulkanValidationReport,
} }
@@ -565,6 +566,7 @@ impl From<VulkanSmokeShutdownReport> for RendererSnapshot {
fn from(report: VulkanSmokeShutdownReport) -> Self { fn from(report: VulkanSmokeShutdownReport) -> Self {
Self { Self {
report: report.renderer_report, report: report.renderer_report,
readback_artifact: report.readback_artifact,
swapchain_recreate_count: report.swapchain_recreate_count, swapchain_recreate_count: report.swapchain_recreate_count,
validation: report.validation, validation: report.validation,
} }
@@ -627,9 +629,24 @@ impl SmokeApp {
.map_err(|err| format!("{}: {err}", self.options.out.display())) .map_err(|err| format!("{}: {err}", self.options.out.display()))
} }
fn write_readback_artifact(&self, artifact: &VulkanReadbackArtifact) -> Result<(), String> {
let stem = self
.options
.out
.file_stem()
.and_then(std::ffi::OsStr::to_str)
.unwrap_or("smoke");
let path = self
.options
.out
.with_file_name(format!("{stem}.readback-b8g8r8a8.raw"));
std::fs::write(&path, &artifact.bytes).map_err(|err| format!("{}: {err}", path.display()))
}
fn live_renderer_snapshot(&self) -> Option<RendererSnapshot> { fn live_renderer_snapshot(&self) -> Option<RendererSnapshot> {
self.renderer.as_ref().map(|renderer| RendererSnapshot { self.renderer.as_ref().map(|renderer| RendererSnapshot {
report: renderer.report().clone(), report: renderer.report().clone(),
readback_artifact: None,
swapchain_recreate_count: renderer.swapchain_recreate_count(), swapchain_recreate_count: renderer.swapchain_recreate_count(),
validation: renderer.validation_report(), validation: renderer.validation_report(),
}) })
@@ -866,6 +883,17 @@ impl SmokeApp {
return; return;
} }
self.final_renderer = Some(final_renderer); self.final_renderer = Some(final_renderer);
if let Some(artifact) = self
.final_renderer
.as_ref()
.and_then(|snapshot| snapshot.readback_artifact.as_ref())
{
if let Err(err) = self.write_readback_artifact(artifact) {
self.error = Some(err);
event_loop.exit();
return;
}
}
let report = match self.render_report("passed", None) { let report = match self.render_report("passed", None) {
Ok(report) => report, Ok(report) => report,
Err(err) => { Err(err) => {
+6
View File
@@ -895,6 +895,12 @@ 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-b8g8r8a8.raw`.
Это concatenated current-swapchain images in Vulkan order, каждый с dimensions
из JSON и четырьмя bytes per pixel; format намеренно указан в имени файла,
потому что bytes не перекодируются. GOG proof produced a 4,147,200-byte file
for two 960x540 images. Артефакт остаётся локальным output и не попадает в Git.
`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