feat(vulkan): export readback artifacts
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user