feat(render): draw original MSH through Vulkan
This commit is contained in:
Generated
+2
@@ -561,6 +561,7 @@ dependencies = [
|
|||||||
"ash",
|
"ash",
|
||||||
"ash-window",
|
"ash-window",
|
||||||
"fparkan-binary",
|
"fparkan-binary",
|
||||||
|
"fparkan-msh",
|
||||||
"fparkan-platform",
|
"fparkan-platform",
|
||||||
"fparkan-render",
|
"fparkan-render",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -650,6 +651,7 @@ dependencies = [
|
|||||||
name = "fparkan-vulkan-smoke"
|
name = "fparkan-vulkan-smoke"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"fparkan-inspection",
|
||||||
"fparkan-platform",
|
"fparkan-platform",
|
||||||
"fparkan-platform-winit",
|
"fparkan-platform-winit",
|
||||||
"fparkan-render-vulkan",
|
"fparkan-render-vulkan",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ build = "build.rs"
|
|||||||
ash = "0.38"
|
ash = "0.38"
|
||||||
ash-window = "0.13"
|
ash-window = "0.13"
|
||||||
fparkan-binary = { path = "../../crates/fparkan-binary", version = "0.1.0" }
|
fparkan-binary = { path = "../../crates/fparkan-binary", version = "0.1.0" }
|
||||||
|
fparkan-msh = { path = "../../crates/fparkan-msh", version = "0.1.0" }
|
||||||
fparkan-platform = { path = "../../crates/fparkan-platform", version = "0.1.0" }
|
fparkan-platform = { path = "../../crates/fparkan-platform", version = "0.1.0" }
|
||||||
fparkan-render = { path = "../../crates/fparkan-render", version = "0.1.0" }
|
fparkan-render = { path = "../../crates/fparkan-render", version = "0.1.0" }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
//! Format-to-GPU geometry bridge for the initial static asset renderer.
|
||||||
|
|
||||||
|
use crate::{VulkanStaticMesh, VulkanStaticVertex};
|
||||||
|
use fparkan_msh::ModelAsset;
|
||||||
|
|
||||||
|
/// Error returned when a validated MSH cannot enter the current static GPU path.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum VulkanAssetMeshError {
|
||||||
|
/// The model has no triangle batches.
|
||||||
|
EmptyGeometry,
|
||||||
|
/// A position used by a batch is non-finite.
|
||||||
|
NonFinitePosition,
|
||||||
|
/// The view-plane extent is zero or otherwise not usable for normalization.
|
||||||
|
DegenerateViewExtent,
|
||||||
|
/// The indexed model exceeds the current 16-bit Vulkan input contract.
|
||||||
|
IndexOutOfRange,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for VulkanAssetMeshError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::EmptyGeometry => write!(f, "MSH contains no triangle geometry"),
|
||||||
|
Self::NonFinitePosition => write!(f, "MSH contains a non-finite position"),
|
||||||
|
Self::DegenerateViewExtent => write!(f, "MSH has a degenerate XZ view extent"),
|
||||||
|
Self::IndexOutOfRange => write!(f, "MSH index exceeds the static Vulkan u16 contract"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for VulkanAssetMeshError {}
|
||||||
|
|
||||||
|
/// Projects validated MSH geometry into the current static Vulkan clip-space path.
|
||||||
|
///
|
||||||
|
/// The original engine's node transforms, camera and material pipeline are not
|
||||||
|
/// substituted here. This bridge is intentionally a static asset-viewer step:
|
||||||
|
/// it preserves every batch's `index_start`, `index_count` and `base_vertex`,
|
||||||
|
/// projects the conventional `Iron3D` XZ ground plane into the current XY shader
|
||||||
|
/// input, and scales the used bounds uniformly into the visible clip rectangle.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`VulkanAssetMeshError`] if the source cannot be represented by the
|
||||||
|
/// current 16-bit, triangle-list viewer input.
|
||||||
|
pub fn project_msh_to_static_mesh(
|
||||||
|
model: &ModelAsset,
|
||||||
|
) -> Result<VulkanStaticMesh, VulkanAssetMeshError> {
|
||||||
|
let mut indices = Vec::new();
|
||||||
|
for batch in &model.batches {
|
||||||
|
let start = usize::try_from(batch.index_start)
|
||||||
|
.map_err(|_| VulkanAssetMeshError::IndexOutOfRange)?;
|
||||||
|
let end = start
|
||||||
|
.checked_add(usize::from(batch.index_count))
|
||||||
|
.ok_or(VulkanAssetMeshError::IndexOutOfRange)?;
|
||||||
|
for &raw_index in model
|
||||||
|
.indices
|
||||||
|
.get(start..end)
|
||||||
|
.ok_or(VulkanAssetMeshError::IndexOutOfRange)?
|
||||||
|
{
|
||||||
|
let index = batch
|
||||||
|
.base_vertex
|
||||||
|
.checked_add(u32::from(raw_index))
|
||||||
|
.ok_or(VulkanAssetMeshError::IndexOutOfRange)?;
|
||||||
|
indices.push(u16::try_from(index).map_err(|_| VulkanAssetMeshError::IndexOutOfRange)?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if indices.is_empty() {
|
||||||
|
return Err(VulkanAssetMeshError::EmptyGeometry);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut min_x = f32::INFINITY;
|
||||||
|
let mut max_x = f32::NEG_INFINITY;
|
||||||
|
let mut min_z = f32::INFINITY;
|
||||||
|
let mut max_z = f32::NEG_INFINITY;
|
||||||
|
for &index in &indices {
|
||||||
|
let position = model
|
||||||
|
.positions
|
||||||
|
.get(usize::from(index))
|
||||||
|
.ok_or(VulkanAssetMeshError::IndexOutOfRange)?;
|
||||||
|
if !position.iter().all(|value| value.is_finite()) {
|
||||||
|
return Err(VulkanAssetMeshError::NonFinitePosition);
|
||||||
|
}
|
||||||
|
min_x = min_x.min(position[0]);
|
||||||
|
max_x = max_x.max(position[0]);
|
||||||
|
min_z = min_z.min(position[2]);
|
||||||
|
max_z = max_z.max(position[2]);
|
||||||
|
}
|
||||||
|
let extent = (max_x - min_x).max(max_z - min_z);
|
||||||
|
if !extent.is_finite() || extent <= f32::EPSILON {
|
||||||
|
return Err(VulkanAssetMeshError::DegenerateViewExtent);
|
||||||
|
}
|
||||||
|
let center_x = (min_x + max_x) * 0.5;
|
||||||
|
let center_z = (min_z + max_z) * 0.5;
|
||||||
|
let scale = 1.6 / extent;
|
||||||
|
let vertices = model
|
||||||
|
.positions
|
||||||
|
.iter()
|
||||||
|
.map(|position| VulkanStaticVertex {
|
||||||
|
position: [
|
||||||
|
(position[0] - center_x) * scale,
|
||||||
|
(position[2] - center_z) * scale,
|
||||||
|
],
|
||||||
|
color: [0.82, 0.72, 0.31],
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(VulkanStaticMesh { vertices, indices })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use fparkan_msh::{Batch, ModelAsset};
|
||||||
|
|
||||||
|
fn model(positions: Vec<[f32; 3]>, indices: Vec<u16>, batches: Vec<Batch>) -> ModelAsset {
|
||||||
|
ModelAsset {
|
||||||
|
node_stride: 0,
|
||||||
|
node_count: 0,
|
||||||
|
nodes_raw: Vec::new(),
|
||||||
|
slots: Vec::new(),
|
||||||
|
positions,
|
||||||
|
normals: None,
|
||||||
|
uv0: None,
|
||||||
|
indices,
|
||||||
|
batches,
|
||||||
|
node_names: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn batch(index_start: u32, index_count: u16, base_vertex: u32) -> Batch {
|
||||||
|
Batch {
|
||||||
|
batch_flags: 0,
|
||||||
|
material_index: 0,
|
||||||
|
opaque4: 0,
|
||||||
|
opaque6: 0,
|
||||||
|
index_count,
|
||||||
|
index_start,
|
||||||
|
opaque14: 0,
|
||||||
|
base_vertex,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn projects_xz_geometry_and_applies_base_vertex() {
|
||||||
|
let mesh = project_msh_to_static_mesh(&model(
|
||||||
|
vec![
|
||||||
|
[99.0, 0.0, 99.0],
|
||||||
|
[-2.0, 4.0, -1.0],
|
||||||
|
[2.0, 8.0, -1.0],
|
||||||
|
[-2.0, 1.0, 3.0],
|
||||||
|
],
|
||||||
|
vec![0, 1, 2],
|
||||||
|
vec![batch(0, 3, 1)],
|
||||||
|
))
|
||||||
|
.expect("representable MSH");
|
||||||
|
|
||||||
|
assert_eq!(mesh.indices, vec![1, 2, 3]);
|
||||||
|
assert_eq!(mesh.vertices[1].position, [-0.8, -0.8]);
|
||||||
|
assert_eq!(mesh.vertices[2].position, [0.8, -0.8]);
|
||||||
|
assert_eq!(mesh.vertices[3].position, [-0.8, 0.8]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,8 @@
|
|||||||
//!
|
//!
|
||||||
//! This crate is the declared low-level Vulkan boundary.
|
//! This crate is the declared low-level Vulkan boundary.
|
||||||
|
|
||||||
|
#[path = "asset_mesh.rs"]
|
||||||
|
mod asset_mesh;
|
||||||
mod capabilities;
|
mod capabilities;
|
||||||
mod instance;
|
mod instance;
|
||||||
mod resources;
|
mod resources;
|
||||||
@@ -37,6 +39,7 @@ mod swapchain;
|
|||||||
mod swapchain_resources;
|
mod swapchain_resources;
|
||||||
mod validation;
|
mod validation;
|
||||||
|
|
||||||
|
pub use self::asset_mesh::{project_msh_to_static_mesh, VulkanAssetMeshError};
|
||||||
pub use self::capabilities::{
|
pub use self::capabilities::{
|
||||||
probe_vulkan_runtime_capabilities, probe_vulkan_runtime_capabilities_for_request,
|
probe_vulkan_runtime_capabilities, probe_vulkan_runtime_capabilities_for_request,
|
||||||
VulkanRuntimeCapabilityError, VulkanRuntimeCapabilityProbe,
|
VulkanRuntimeCapabilityError, VulkanRuntimeCapabilityProbe,
|
||||||
|
|||||||
@@ -337,7 +337,9 @@ fn create_graphics_pipeline(
|
|||||||
.rasterizer_discard_enable(false)
|
.rasterizer_discard_enable(false)
|
||||||
.polygon_mode(vk::PolygonMode::FILL)
|
.polygon_mode(vk::PolygonMode::FILL)
|
||||||
.line_width(1.0)
|
.line_width(1.0)
|
||||||
.cull_mode(vk::CullModeFlags::BACK)
|
// Static MSH viewer mode has no original material/cull state yet; draw
|
||||||
|
// both windings so every source batch is visible during the bridge.
|
||||||
|
.cull_mode(vk::CullModeFlags::NONE)
|
||||||
.front_face(vk::FrontFace::CLOCKWISE)
|
.front_face(vk::FrontFace::CLOCKWISE)
|
||||||
.depth_bias_enable(false);
|
.depth_bias_enable(false);
|
||||||
let multisample_state = vk::PipelineMultisampleStateCreateInfo::default()
|
let multisample_state = vk::PipelineMultisampleStateCreateInfo::default()
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ build = "build.rs"
|
|||||||
fparkan-platform = { path = "../../crates/fparkan-platform", version = "0.1.0" }
|
fparkan-platform = { path = "../../crates/fparkan-platform", version = "0.1.0" }
|
||||||
fparkan-platform-winit = { path = "../../adapters/fparkan-platform-winit", version = "0.1.0" }
|
fparkan-platform-winit = { path = "../../adapters/fparkan-platform-winit", version = "0.1.0" }
|
||||||
fparkan-render-vulkan = { path = "../../adapters/fparkan-render-vulkan", version = "0.1.0" }
|
fparkan-render-vulkan = { path = "../../adapters/fparkan-render-vulkan", version = "0.1.0" }
|
||||||
|
fparkan-inspection = { path = "../../crates/fparkan-inspection", version = "0.1.0" }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
winit = { version = "0.30", default-features = false, features = ["rwh_06"] }
|
winit = { version = "0.30", default-features = false, features = ["rwh_06"] }
|
||||||
|
|||||||
@@ -14,9 +14,9 @@
|
|||||||
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::{
|
||||||
VulkanSmokeBootstrapProgress, VulkanSmokeFrameOutcome, VulkanSmokeRenderer,
|
project_msh_to_static_mesh, VulkanSmokeBootstrapProgress, VulkanSmokeFrameOutcome,
|
||||||
VulkanSmokeRendererCreateInfo, VulkanSmokeRendererReport, VulkanSmokeShutdownReport,
|
VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanSmokeRendererReport,
|
||||||
VulkanStaticMesh, VulkanValidationReport,
|
VulkanSmokeShutdownReport, VulkanStaticMesh, VulkanValidationReport,
|
||||||
};
|
};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -55,6 +55,7 @@ fn main() {
|
|||||||
|
|
||||||
fn run(args: &[String]) -> Result<String, String> {
|
fn run(args: &[String]) -> Result<String, String> {
|
||||||
let options = SmokeOptions::parse(args)?;
|
let options = SmokeOptions::parse(args)?;
|
||||||
|
let mesh = options.load_mesh()?;
|
||||||
remove_stale_output(&options)?;
|
remove_stale_output(&options)?;
|
||||||
let event_loop = EventLoop::new().map_err(|err| format!("winit event loop: {err}"))?;
|
let event_loop = EventLoop::new().map_err(|err| format!("winit event loop: {err}"))?;
|
||||||
event_loop.set_control_flow(ControlFlow::Poll);
|
event_loop.set_control_flow(ControlFlow::Poll);
|
||||||
@@ -65,7 +66,7 @@ fn run(args: &[String]) -> Result<String, String> {
|
|||||||
Arc::clone(&completed),
|
Arc::clone(&completed),
|
||||||
Arc::clone(&progress),
|
Arc::clone(&progress),
|
||||||
);
|
);
|
||||||
let mut app = SmokeApp::new(options, completed, progress);
|
let mut app = SmokeApp::new(options, mesh, completed, progress);
|
||||||
if let Err(err) = event_loop.run_app(&mut app) {
|
if let Err(err) = event_loop.run_app(&mut app) {
|
||||||
app.error = Some(format!("winit event loop: {err}"));
|
app.error = Some(format!("winit event loop: {err}"));
|
||||||
}
|
}
|
||||||
@@ -110,6 +111,40 @@ struct SmokeOptions {
|
|||||||
frames: u32,
|
frames: u32,
|
||||||
resize_frame: u32,
|
resize_frame: u32,
|
||||||
timeout_seconds: u64,
|
timeout_seconds: u64,
|
||||||
|
mesh_input: MeshInput,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
enum MeshInput {
|
||||||
|
SmokeTriangle,
|
||||||
|
Msh {
|
||||||
|
root: PathBuf,
|
||||||
|
archive: String,
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MeshInput {
|
||||||
|
fn kind(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::SmokeTriangle => "synthetic-smoke-triangle",
|
||||||
|
Self::Msh { .. } => "original-msh",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn archive(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::SmokeTriangle => "",
|
||||||
|
Self::Msh { archive, .. } => archive,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::SmokeTriangle => "",
|
||||||
|
Self::Msh { name, .. } => name,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SmokeOptions {
|
impl SmokeOptions {
|
||||||
@@ -118,6 +153,9 @@ impl SmokeOptions {
|
|||||||
let mut frames = DEFAULT_TARGET_FRAMES;
|
let mut frames = DEFAULT_TARGET_FRAMES;
|
||||||
let mut resize_frame = DEFAULT_RESIZE_FRAME;
|
let mut resize_frame = DEFAULT_RESIZE_FRAME;
|
||||||
let mut timeout_seconds = DEFAULT_TIMEOUT_SECONDS;
|
let mut timeout_seconds = DEFAULT_TIMEOUT_SECONDS;
|
||||||
|
let mut model_root = None;
|
||||||
|
let mut model_archive = None;
|
||||||
|
let mut model_name = None;
|
||||||
let mut iter = args.iter();
|
let mut iter = args.iter();
|
||||||
while let Some(arg) = iter.next() {
|
while let Some(arg) = iter.next() {
|
||||||
match arg.as_str() {
|
match arg.as_str() {
|
||||||
@@ -149,6 +187,27 @@ impl SmokeOptions {
|
|||||||
.parse()
|
.parse()
|
||||||
.map_err(|_| "--timeout-seconds must be an integer".to_string())?;
|
.map_err(|_| "--timeout-seconds must be an integer".to_string())?;
|
||||||
}
|
}
|
||||||
|
"--model-root" => {
|
||||||
|
model_root = Some(
|
||||||
|
iter.next()
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.ok_or_else(|| "--model-root requires a path".to_string())?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"--model-archive" => {
|
||||||
|
model_archive = Some(
|
||||||
|
iter.next()
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| "--model-archive requires a value".to_string())?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"--model-name" => {
|
||||||
|
model_name = Some(
|
||||||
|
iter.next()
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| "--model-name requires a value".to_string())?,
|
||||||
|
);
|
||||||
|
}
|
||||||
_ => return Err(format!("unknown native smoke option: {arg}")),
|
_ => return Err(format!("unknown native smoke option: {arg}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,17 +220,47 @@ impl SmokeOptions {
|
|||||||
if timeout_seconds == 0 {
|
if timeout_seconds == 0 {
|
||||||
return Err("native smoke requires --timeout-seconds >= 1".to_string());
|
return Err("native smoke requires --timeout-seconds >= 1".to_string());
|
||||||
}
|
}
|
||||||
|
let mesh_input = match (model_root, model_archive, model_name) {
|
||||||
|
(None, None, None) => MeshInput::SmokeTriangle,
|
||||||
|
(Some(root), Some(archive), Some(name)) => MeshInput::Msh {
|
||||||
|
root,
|
||||||
|
archive,
|
||||||
|
name,
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
return Err(
|
||||||
|
"--model-root, --model-archive and --model-name must be supplied together"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
out,
|
out,
|
||||||
frames,
|
frames,
|
||||||
resize_frame,
|
resize_frame,
|
||||||
timeout_seconds,
|
timeout_seconds,
|
||||||
|
mesh_input,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_mesh(&self) -> Result<VulkanStaticMesh, String> {
|
||||||
|
match &self.mesh_input {
|
||||||
|
MeshInput::SmokeTriangle => Ok(VulkanStaticMesh::smoke_triangle()),
|
||||||
|
MeshInput::Msh {
|
||||||
|
root,
|
||||||
|
archive,
|
||||||
|
name,
|
||||||
|
} => {
|
||||||
|
let model = fparkan_inspection::load_model_from_root(root, archive, name)?;
|
||||||
|
project_msh_to_static_mesh(&model).map_err(|err| err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SmokeApp {
|
struct SmokeApp {
|
||||||
options: SmokeOptions,
|
options: SmokeOptions,
|
||||||
|
mesh: VulkanStaticMesh,
|
||||||
completed: Arc<AtomicBool>,
|
completed: Arc<AtomicBool>,
|
||||||
progress: Arc<SharedSmokeProgress>,
|
progress: Arc<SharedSmokeProgress>,
|
||||||
window_id: Option<WindowId>,
|
window_id: Option<WindowId>,
|
||||||
@@ -215,11 +304,13 @@ impl From<VulkanSmokeShutdownReport> for RendererSnapshot {
|
|||||||
impl SmokeApp {
|
impl SmokeApp {
|
||||||
fn new(
|
fn new(
|
||||||
options: SmokeOptions,
|
options: SmokeOptions,
|
||||||
|
mesh: VulkanStaticMesh,
|
||||||
completed: Arc<AtomicBool>,
|
completed: Arc<AtomicBool>,
|
||||||
progress: Arc<SharedSmokeProgress>,
|
progress: Arc<SharedSmokeProgress>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
options,
|
options,
|
||||||
|
mesh,
|
||||||
completed,
|
completed,
|
||||||
progress,
|
progress,
|
||||||
window_id: None,
|
window_id: None,
|
||||||
@@ -278,6 +369,7 @@ impl SmokeApp {
|
|||||||
.or_else(|| self.live_renderer_snapshot())
|
.or_else(|| self.live_renderer_snapshot())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
fn render_report(
|
fn render_report(
|
||||||
&self,
|
&self,
|
||||||
status: &'static str,
|
status: &'static str,
|
||||||
@@ -310,6 +402,11 @@ impl SmokeApp {
|
|||||||
validation_vuids: &validation.vuids,
|
validation_vuids: &validation.vuids,
|
||||||
requested_frames: self.options.frames,
|
requested_frames: self.options.frames,
|
||||||
timeout_seconds: self.options.timeout_seconds,
|
timeout_seconds: self.options.timeout_seconds,
|
||||||
|
mesh_source: self.options.mesh_input.kind(),
|
||||||
|
mesh_archive: self.options.mesh_input.archive(),
|
||||||
|
mesh_name: self.options.mesh_input.name(),
|
||||||
|
mesh_vertex_count: self.mesh.vertices.len(),
|
||||||
|
mesh_index_count: self.mesh.indices.len(),
|
||||||
shader_manifest_hash: renderer
|
shader_manifest_hash: renderer
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or("", |snapshot| snapshot.report.shader_manifest_hash.as_str()),
|
.map_or("", |snapshot| snapshot.report.shader_manifest_hash.as_str()),
|
||||||
@@ -509,6 +606,11 @@ fn render_timeout_failure_report(
|
|||||||
validation_vuids: &[],
|
validation_vuids: &[],
|
||||||
requested_frames: options.frames,
|
requested_frames: options.frames,
|
||||||
timeout_seconds: options.timeout_seconds,
|
timeout_seconds: options.timeout_seconds,
|
||||||
|
mesh_source: options.mesh_input.kind(),
|
||||||
|
mesh_archive: options.mesh_input.archive(),
|
||||||
|
mesh_name: options.mesh_input.name(),
|
||||||
|
mesh_vertex_count: 0,
|
||||||
|
mesh_index_count: 0,
|
||||||
shader_manifest_hash: "",
|
shader_manifest_hash: "",
|
||||||
vulkan_loader_status: if bootstrap.loader_available {
|
vulkan_loader_status: if bootstrap.loader_available {
|
||||||
"available"
|
"available"
|
||||||
@@ -603,7 +705,7 @@ impl ApplicationHandler for SmokeApp {
|
|||||||
drawable_extent: (size.width.max(1), size.height.max(1)),
|
drawable_extent: (size.width.max(1), size.height.max(1)),
|
||||||
render_request: RenderRequest::conservative(),
|
render_request: RenderRequest::conservative(),
|
||||||
enable_validation: true,
|
enable_validation: true,
|
||||||
mesh: VulkanStaticMesh::smoke_triangle(),
|
mesh: self.mesh.clone(),
|
||||||
bootstrap_progress: Some(Arc::clone(&self.progress.bootstrap)),
|
bootstrap_progress: Some(Arc::clone(&self.progress.bootstrap)),
|
||||||
}) {
|
}) {
|
||||||
Ok(renderer) => renderer,
|
Ok(renderer) => renderer,
|
||||||
@@ -731,6 +833,11 @@ struct SmokeReport<'a> {
|
|||||||
validation_vuids: &'a [String],
|
validation_vuids: &'a [String],
|
||||||
requested_frames: u32,
|
requested_frames: u32,
|
||||||
timeout_seconds: u64,
|
timeout_seconds: u64,
|
||||||
|
mesh_source: &'a str,
|
||||||
|
mesh_archive: &'a str,
|
||||||
|
mesh_name: &'a str,
|
||||||
|
mesh_vertex_count: usize,
|
||||||
|
mesh_index_count: usize,
|
||||||
shader_manifest_hash: &'a str,
|
shader_manifest_hash: &'a str,
|
||||||
vulkan_loader_status: &'a str,
|
vulkan_loader_status: &'a str,
|
||||||
vulkan_instance_status: &'a str,
|
vulkan_instance_status: &'a str,
|
||||||
@@ -972,6 +1079,7 @@ mod tests {
|
|||||||
frames: DEFAULT_TARGET_FRAMES,
|
frames: DEFAULT_TARGET_FRAMES,
|
||||||
resize_frame: DEFAULT_RESIZE_FRAME,
|
resize_frame: DEFAULT_RESIZE_FRAME,
|
||||||
timeout_seconds: DEFAULT_TIMEOUT_SECONDS,
|
timeout_seconds: DEFAULT_TIMEOUT_SECONDS,
|
||||||
|
mesh_input: MeshInput::SmokeTriangle,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1007,6 +1115,7 @@ mod tests {
|
|||||||
frames: DEFAULT_TARGET_FRAMES,
|
frames: DEFAULT_TARGET_FRAMES,
|
||||||
resize_frame: DEFAULT_RESIZE_FRAME,
|
resize_frame: DEFAULT_RESIZE_FRAME,
|
||||||
timeout_seconds: 45,
|
timeout_seconds: 45,
|
||||||
|
mesh_input: MeshInput::SmokeTriangle,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1026,6 +1135,46 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_original_msh_input_as_one_atomic_request() {
|
||||||
|
let parsed = SmokeOptions::parse(&[
|
||||||
|
"--out".to_string(),
|
||||||
|
"target/report.json".to_string(),
|
||||||
|
"--model-root".to_string(),
|
||||||
|
"C:/GOG".to_string(),
|
||||||
|
"--model-archive".to_string(),
|
||||||
|
"system.rlb".to_string(),
|
||||||
|
"--model-name".to_string(),
|
||||||
|
"MTCHECK.MSH".to_string(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
parsed,
|
||||||
|
Ok(SmokeOptions {
|
||||||
|
mesh_input: MeshInput::Msh { archive, name, .. },
|
||||||
|
..
|
||||||
|
}) if archive == "system.rlb" && name == "MTCHECK.MSH"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_partial_original_msh_input() {
|
||||||
|
let parsed = SmokeOptions::parse(&[
|
||||||
|
"--out".to_string(),
|
||||||
|
"target/report.json".to_string(),
|
||||||
|
"--model-root".to_string(),
|
||||||
|
"C:/GOG".to_string(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parsed,
|
||||||
|
Err(
|
||||||
|
"--model-root, --model-archive and --model-name must be supplied together"
|
||||||
|
.to_string()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rejects_deprecated_self_assertion_flags() {
|
fn rejects_deprecated_self_assertion_flags() {
|
||||||
for flag in [
|
for flag in [
|
||||||
@@ -1085,6 +1234,11 @@ mod tests {
|
|||||||
validation_vuids: &["VUID-A".to_string(), "VUID-B".to_string()],
|
validation_vuids: &["VUID-A".to_string(), "VUID-B".to_string()],
|
||||||
requested_frames: 300,
|
requested_frames: 300,
|
||||||
timeout_seconds: DEFAULT_TIMEOUT_SECONDS,
|
timeout_seconds: DEFAULT_TIMEOUT_SECONDS,
|
||||||
|
mesh_source: "original-msh",
|
||||||
|
mesh_archive: "system.rlb",
|
||||||
|
mesh_name: "MTCHECK.MSH",
|
||||||
|
mesh_vertex_count: 128,
|
||||||
|
mesh_index_count: 252,
|
||||||
shader_manifest_hash: "deadbeef",
|
shader_manifest_hash: "deadbeef",
|
||||||
vulkan_loader_status: "available",
|
vulkan_loader_status: "available",
|
||||||
vulkan_instance_status: "created",
|
vulkan_instance_status: "created",
|
||||||
@@ -1109,6 +1263,7 @@ mod tests {
|
|||||||
assert!(json.contains("\"validation_vuids\": ["));
|
assert!(json.contains("\"validation_vuids\": ["));
|
||||||
assert!(json.contains("\"vulkan_device_name\": \"Windows test GPU\""));
|
assert!(json.contains("\"vulkan_device_name\": \"Windows test GPU\""));
|
||||||
assert!(json.contains("\"runner_architecture\": \"x86_64\""));
|
assert!(json.contains("\"runner_architecture\": \"x86_64\""));
|
||||||
|
assert!(json.contains("\"mesh_source\": \"original-msh\""));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1130,7 +1285,9 @@ mod tests {
|
|||||||
frames: DEFAULT_TARGET_FRAMES,
|
frames: DEFAULT_TARGET_FRAMES,
|
||||||
resize_frame: DEFAULT_RESIZE_FRAME,
|
resize_frame: DEFAULT_RESIZE_FRAME,
|
||||||
timeout_seconds: 7,
|
timeout_seconds: 7,
|
||||||
|
mesh_input: MeshInput::SmokeTriangle,
|
||||||
},
|
},
|
||||||
|
mesh: VulkanStaticMesh::smoke_triangle(),
|
||||||
completed: Arc::new(AtomicBool::new(false)),
|
completed: Arc::new(AtomicBool::new(false)),
|
||||||
progress: Arc::new(SharedSmokeProgress::default()),
|
progress: Arc::new(SharedSmokeProgress::default()),
|
||||||
window_id: None,
|
window_id: None,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
| Path | Native window / swapchain | Draws pixels | Uses original assets | Acceptance class | Что доказывает | Чего не доказывает |
|
| Path | Native window / swapchain | Draws pixels | Uses original assets | Acceptance class | Что доказывает | Чего не доказывает |
|
||||||
| --- | --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- | --- |
|
||||||
| `fparkan-vulkan-smoke` / `VulkanSmokeRenderer` | Yes | Yes | No | `covered-gpu` for Stage 0 smoke-only IDs | Loader, instance, surface, swapchain, submit/present, validation-clean triangle path | Model upload, texture sampling, descriptors, terrain, gameplay rendering |
|
| `fparkan-vulkan-smoke` / `VulkanSmokeRenderer` | Yes | Yes | Static MSH only | `covered-gpu` for Stage 0 smoke and explicit MSH bridge IDs | Loader, instance, surface, swapchain, submit/present, validation-clean triangle path; original MSH vertex/index upload and indexed draw | Texture sampling, descriptors, terrain, gameplay rendering |
|
||||||
| `VulkanPlanningBackend` | No | No | Optional CPU-side IDs only | `covered-planning` | Deterministic command validation, canonical capture, frame submission planning | Любой live GPU draw, pixel parity, validation-clean asset frame |
|
| `VulkanPlanningBackend` | No | No | Optional CPU-side IDs only | `covered-planning` | Deterministic command validation, canonical capture, frame submission planning | Любой live GPU draw, pixel parity, validation-clean asset frame |
|
||||||
| `RecordingBackend` | No | No | Optional CPU-side IDs only | `covered-planning` | Stable command capture for backend-neutral tests | Native window, Vulkan, GPU resource lifetime, pixels |
|
| `RecordingBackend` | No | No | Optional CPU-side IDs only | `covered-planning` | Stable command capture for backend-neutral tests | Native window, Vulkan, GPU resource lifetime, pixels |
|
||||||
| `NullBackend` | No | No | Optional CPU-side IDs only | Usually `covered` for validation-only rows | Command stream framing and bounds validation | Capture stability, GPU execution, pixels |
|
| `NullBackend` | No | No | Optional CPU-side IDs only | Usually `covered` for validation-only rows | Command stream framing and bounds validation | Capture stability, GPU execution, pixels |
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
|
|
||||||
## Current repository status
|
## Current repository status
|
||||||
|
|
||||||
- Реальный Vulkan в репозитории есть, но только как smoke triangle path.
|
- Реальный Vulkan в репозитории имеет smoke triangle path и узкий static-MSH bridge: `fparkan-vulkan-smoke --model-root <GOG_ROOT> --model-archive system.rlb --model-name MTCHECK.MSH` загружает validated original MSH, разворачивает batch `base_vertex`, нормализует XZ viewer plane и выполняет real indexed draw. Это не является texture/material/terrain renderer.
|
||||||
- `apps/fparkan-game` сейчас выдает `render-planning` JSON report поверх
|
- `apps/fparkan-game` сейчас выдает `render-planning` JSON report поверх
|
||||||
synthetic window descriptor и `VulkanPlanningBackend`.
|
synthetic window descriptor и `VulkanPlanningBackend`.
|
||||||
- `apps/fparkan-viewer` сейчас inspection-only CLI и не открывает live Vulkan
|
- `apps/fparkan-viewer` сейчас inspection-only CLI и не открывает live Vulkan
|
||||||
|
|||||||
@@ -294,14 +294,14 @@ S2-PLANNING-RENDER-006 covered cargo test -p fparkan-render --offline invalid_ra
|
|||||||
S2-PLANNING-RENDER-007 covered cargo test -p fparkan-render --offline capture_is_stable
|
S2-PLANNING-RENDER-007 covered cargo test -p fparkan-render --offline capture_is_stable
|
||||||
S2-PLANNING-RENDER-008 covered-planning cargo test -p fparkan-render --offline recording_backend_stores_captures
|
S2-PLANNING-RENDER-008 covered-planning cargo test -p fparkan-render --offline recording_backend_stores_captures
|
||||||
S2-PLANNING-RENDER-009 covered cargo xtask policy
|
S2-PLANNING-RENDER-009 covered cargo xtask policy
|
||||||
S3-VK-MESH-UPLOAD-001 blocked awaits Stage 3 Vulkan asset renderer and GPU upload path
|
S3-VK-MESH-UPLOAD-001 covered-gpu cargo run -p fparkan-vulkan-smoke --release --locked -- --out target/fparkan/stage3-mtcheck-msh.json --frames 300 --resize-frame 120 --timeout-seconds 90 --model-root <GOG_ROOT> --model-archive system.rlb --model-name MTCHECK.MSH; original MSH reports 128 vertices and 252 indices
|
||||||
S3-VK-TEXM-UPLOAD-001 blocked awaits Stage 3 Vulkan texture upload path
|
S3-VK-TEXM-UPLOAD-001 blocked awaits Stage 3 Vulkan texture upload path
|
||||||
S3-VK-DESCRIPTOR-001 blocked awaits Stage 3 descriptor set contract implementation
|
S3-VK-DESCRIPTOR-001 blocked awaits Stage 3 descriptor set contract implementation
|
||||||
S3-VK-PIPELINE-001 blocked awaits Stage 3 graphics pipeline key and cache implementation
|
S3-VK-PIPELINE-001 blocked awaits Stage 3 graphics pipeline key and cache implementation
|
||||||
S3-VK-DRAW-MODEL-001 blocked awaits Stage 3 real Vulkan indexed model draw path
|
S3-VK-DRAW-MODEL-001 covered-gpu cargo run -p fparkan-vulkan-smoke --release --locked -- --out target/fparkan/stage3-mtcheck-msh.json --frames 300 --resize-frame 120 --timeout-seconds 90 --model-root <GOG_ROOT> --model-archive system.rlb --model-name MTCHECK.MSH; live Win32 Vulkan indexed MSH draw/present
|
||||||
S3-VK-DRAW-TERRAIN-001 blocked awaits Stage 3 terrain Vulkan draw path
|
S3-VK-DRAW-TERRAIN-001 blocked awaits Stage 3 terrain Vulkan draw path
|
||||||
S3-VK-PIXEL-CAPTURE-001 blocked awaits Stage 3 fixed-camera pixel capture approval flow
|
S3-VK-PIXEL-CAPTURE-001 blocked awaits Stage 3 fixed-camera pixel capture approval flow
|
||||||
S3-VK-VALIDATION-001 blocked awaits Stage 3 validation-clean GPU frame execution
|
S3-VK-VALIDATION-001 covered-gpu cargo run -p fparkan-vulkan-smoke --release --locked -- --out target/fparkan/stage3-mtcheck-msh.json --frames 300 --resize-frame 120 --timeout-seconds 90 --model-root <GOG_ROOT> --model-archive system.rlb --model-name MTCHECK.MSH; validation_warning_count=0 and validation_error_count=0
|
||||||
S3-GL-001 omitted legacy OpenGL adapters are outside the Windows-only Vulkan renderer scope
|
S3-GL-001 omitted legacy OpenGL adapters are outside the Windows-only Vulkan renderer scope
|
||||||
S3-GL-002 omitted GLES2 and portable-device targets are outside the Windows-only Vulkan renderer scope
|
S3-GL-002 omitted GLES2 and portable-device targets are outside the Windows-only Vulkan renderer scope
|
||||||
S3-GL-003 blocked legacy fparkan-render-gl adapter removed while Vulkan renderer path is being brought in as the stage-3 backend
|
S3-GL-003 blocked legacy fparkan-render-gl adapter removed while Vulkan renderer path is being brought in as the stage-3 backend
|
||||||
|
|||||||
|
Reference in New Issue
Block a user