diff options
| author | Valentin Popov <valentin@popov.link> | 2026-09-06 04:22:12 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-09-06 04:22:12 +0300 |
| commit | aa51f3574d3d4d4e0fa59653eedd718fceb5d1bd (patch) | |
| tree | 9a43f6a3f3c86767e4e2b7316051562d96789f06 /apps/fparkan-game | |
| parent | da03e77f53bb33b2fe203ac45578289953a1b524 (diff) | |
| download | fparkan-aa51f3574d3d4d4e0fa59653eedd718fceb5d1bd.tar.xz fparkan-aa51f3574d3d4d4e0fa59653eedd718fceb5d1bd.zip | |
chore: simplify project to engine docs and tests
Remove planning and acceptance scaffolding while retaining the native Vulkan mission preview, format readers, runtime algorithms, and ordinary Rust tests. Keep the book aligned with the runnable project and validate checked-in shaders without generated tool metadata.
Diffstat (limited to 'apps/fparkan-game')
| -rw-r--r-- | apps/fparkan-game/Cargo.toml | 22 | ||||
| -rw-r--r-- | apps/fparkan-game/src/main.rs | 1041 |
2 files changed, 103 insertions, 960 deletions
diff --git a/apps/fparkan-game/Cargo.toml b/apps/fparkan-game/Cargo.toml index 49e2cad..780a21c 100644 --- a/apps/fparkan-game/Cargo.toml +++ b/apps/fparkan-game/Cargo.toml @@ -6,20 +6,14 @@ license.workspace = true repository.workspace = true [dependencies] -fparkan-assets = { path = "../../crates/fparkan-assets", version = "0.1.0" } -fparkan-inspection = { path = "../../crates/fparkan-inspection", version = "0.1.0" } -fparkan-path = { path = "../../crates/fparkan-path", 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-platform-winit = { path = "../../adapters/fparkan-platform-winit", version = "0.1.0" } -fparkan-render-vulkan = { path = "../../adapters/fparkan-render-vulkan", version = "0.1.0" } -fparkan-runtime = { path = "../../crates/fparkan-runtime", version = "0.1.0" } -fparkan-terrain = { path = "../../crates/fparkan-terrain", version = "0.1.0" } -fparkan-vfs = { path = "../../crates/fparkan-vfs", version = "0.1.0" } -fparkan-world = { path = "../../crates/fparkan-world", version = "0.1.0" } +fparkan-assets = { path = "../../crates/fparkan-assets" } +fparkan-path = { path = "../../crates/fparkan-path" } +fparkan-render = { path = "../../crates/fparkan-render" } +fparkan-platform-winit = { path = "../../adapters/fparkan-platform-winit" } +fparkan-render-vulkan = { path = "../../adapters/fparkan-render-vulkan" } +fparkan-runtime = { path = "../../crates/fparkan-runtime" } +fparkan-terrain = { path = "../../crates/fparkan-terrain" } +fparkan-vfs = { path = "../../crates/fparkan-vfs" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" winit = { version = "0.30", default-features = false, features = ["rwh_06"] } - -[lints] -workspace = true diff --git a/apps/fparkan-game/src/main.rs b/apps/fparkan-game/src/main.rs index af7fe1d..2130bf5 100644 --- a/apps/fparkan-game/src/main.rs +++ b/apps/fparkan-game/src/main.rs @@ -1,60 +1,32 @@ #![forbid(unsafe_code)] -#![cfg_attr( - test, - allow( - clippy::cast_possible_truncation, - clippy::cast_possible_wrap, - clippy::cast_precision_loss, - clippy::expect_used, - clippy::float_cmp, - clippy::identity_op, - clippy::too_many_lines, - clippy::uninlined_format_args, - clippy::map_unwrap_or, - clippy::needless_raw_string_hashes, - clippy::semicolon_if_nothing_returned, - clippy::type_complexity, - clippy::panic, - clippy::unwrap_used - ) -)] -#![allow(clippy::print_stderr, clippy::print_stdout)] + //! `FParkan` render-planning composition root. -use fparkan_assets::{PreparedTextureUsage, PreparedVisual}; -use fparkan_inspection::load_standalone_wear_material_texture_mip0_rgba8_from_root; +use fparkan_assets::{ + load_standalone_wear_material_texture_mip0_rgba8_from_root, PreparedTextureUsage, + PreparedVisual, +}; use fparkan_path::ResourceName; -use fparkan_platform::WindowPort; use fparkan_platform_winit::{window_native_handles, WinitWindow, WinitWindowPlan}; -use fparkan_render::{ - build_commands, CameraSnapshot, DrawId, GpuMaterialId, GpuMeshId, IndexRange, - LegacyD3d7Projection, LegacyIron3dEulerTransform, RawCameraTransform, RenderBackend, - RenderCommand, RenderCommandList, RenderPhase, RenderProfile, RenderSnapshot, - RenderSnapshotDraw, -}; +use fparkan_render::{LegacyD3d7Projection, LegacyIron3dEulerTransform, RawCameraTransform}; use fparkan_render_vulkan::{ project_land_msh_to_static_mesh_in_legacy_world_space, project_land_msh_to_static_mesh_in_xy_frame, project_msh_to_static_mesh_in_world_space_with_node_fallback_poses, project_msh_to_static_mesh_in_world_space_with_node_sampled_poses, project_msh_to_static_mesh_in_xy_frame_with_node_fallback_poses, - project_msh_to_static_mesh_in_xy_frame_with_node_sampled_poses, VulkanPlanningBackend, - VulkanSmokeFrameOutcome, VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, - VulkanStaticCamera, VulkanStaticMaterial, VulkanStaticMesh, VulkanStaticTexture, - VulkanStaticXyFrame, + project_msh_to_static_mesh_in_xy_frame_with_node_sampled_poses, VulkanSmokeFrameOutcome, + VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanStaticCamera, VulkanStaticMaterial, + VulkanStaticMesh, VulkanStaticTexture, VulkanStaticXyFrame, }; use fparkan_runtime::{ - create, frame, load_mission, load_mission_static_preview, load_mission_static_preview_roots, - load_mission_static_preview_roots_with_progress, load_mission_with_progress, - loaded_mission_assets, loaded_mission_object_drafts, loaded_terrain, EngineConfig, EngineMode, - EngineServices, MissionAssets, MissionLoadPhase, MissionObjectDraft, MissionRequest, + create, load_mission_static_preview, load_mission_static_preview_roots, loaded_mission_assets, + loaded_mission_object_drafts, loaded_terrain, EngineServices, MissionAssets, + MissionObjectDraft, MissionRequest, }; use fparkan_terrain::TerrainMaterialLayers; use fparkan_terrain::TerrainWorld; use fparkan_vfs::DirectoryVfs; -#[cfg(test)] -use fparkan_world::OriginalObjectId; -use fparkan_world::{TransformState, WorldSnapshot}; use serde::Deserialize; use std::num::NonZeroUsize; use std::path::PathBuf; @@ -83,91 +55,42 @@ fn main() { fn run(args: &[String]) -> Result<String, String> { let args = Args::parse(args)?; let services = EngineServices::new(Arc::new(DirectoryVfs::new(&args.root))); - let mut engine = create( - EngineConfig { - mode: EngineMode::Rendered, - }, - services, - ) - .map_err(|err| err.to_string())?; + let mut engine = create(services).map_err(|err| err.to_string())?; let loaded = load_requested_mission(&mut engine, &args)?; - if args.backend == RenderBackendMode::StaticVulkan { - let mission_assets = loaded_mission_assets(&engine) - .ok_or_else(|| "mission assets are unavailable after loading".to_string())?; - let terrain = loaded_terrain(&engine) - .ok_or_else(|| "mission terrain is unavailable after loading".to_string())?; - let roots = loaded_mission_object_drafts(&engine) - .map(|drafts| &drafts[..args.preview_roots.get().min(drafts.len())]) - .filter(|roots| !roots.is_empty()) - .ok_or_else(|| { - "selected mission object drafts are unavailable after loading".to_string() - })?; - let camera = args - .legacy_camera_capture - .as_deref() - .map(load_legacy_camera_capture) - .transpose()?; - let preview = static_preview_mesh_and_materials( - mission_assets, - terrain, - roots, - camera, - args.static_animation_frame, - args.static_material_phase, - &args.root, - &loaded.land_msh_path, - )?; - return run_static_vulkan_mode( - preview, - roots.len(), - args.frames, - &args.mission, - loaded.object_count, - args.readback_out.as_deref(), - ); - } - - let mut backend = VulkanPlanningBackend::new(); - let _request = WinitWindow::default_render_request(); - let window = WinitWindow::synthetic(1280, 720); - let _ = window.drawable_size(); - let _ = window.handle(); - let mut last_draw_count = 0usize; - let mut last_tick = 0u64; - let mut last_hash = [0u8; 32]; - for _ in 0..args.frames { - let result = frame(&mut engine).map_err(|err| err.to_string())?; - last_tick = result.snapshot.tick.0; - last_hash = result.snapshot.hash.0; - let mission_assets = loaded_mission_assets(&engine); - let mission_drafts = fparkan_runtime::loaded_mission_object_drafts(&engine); - let commands = - render_snapshot_commands_with_assets(&result.snapshot, mission_assets, mission_drafts) - .map_err(|err| format!("render snapshot: {err}"))?; - last_draw_count = commands - .commands - .iter() - .filter(|command| matches!(command, RenderCommand::Draw(_))) - .count(); - backend - .execute(&commands) - .map_err(|err| format!("render backend: {err}"))?; - } - - let capture_report = backend.report(); - - Ok(format!( - "{{\"report_kind\":\"render-planning\",\"backend\":\"vulkan-planning\",\"window\":\"synthetic\",\"mission\":{},\"objects\":{},\"frames\":{},\"tick\":{},\"draws\":{},\"submission_plans\":{},\"last_command_capture_bytes\":{},\"hash\":{}}}", - json_string(&args.mission), - loaded.object_count, + let mission_assets = loaded_mission_assets(&engine) + .ok_or_else(|| "mission assets are unavailable after loading".to_string())?; + let terrain = loaded_terrain(&engine) + .ok_or_else(|| "mission terrain is unavailable after loading".to_string())?; + let roots = loaded_mission_object_drafts(&engine) + .map(|drafts| &drafts[..args.preview_roots.get().min(drafts.len())]) + .filter(|roots| !roots.is_empty()) + .ok_or_else(|| { + "selected mission object drafts are unavailable after loading".to_string() + })?; + let camera = args + .legacy_camera_capture + .as_deref() + .map(load_legacy_camera_capture) + .transpose()?; + let preview = static_preview_mesh_and_materials( + mission_assets, + terrain, + roots, + camera, + args.static_animation_frame, + args.static_material_phase, + &args.root, + &loaded.land_msh_path, + )?; + run_static_vulkan_mode( + preview, args.frames, - last_tick, - last_draw_count, - capture_report.execution.submission_plans, - capture_report.execution.last_capture_size, - json_hash(&last_hash) - )) + &args.mission, + loaded.object_count, + args.readback_out.as_deref(), + args.validation, + ) } fn load_requested_mission( @@ -177,62 +100,10 @@ fn load_requested_mission( let request = MissionRequest { key: args.mission.clone(), }; - if let Some(progress_path) = args.load_progress.as_ref() { - prepare_load_progress_path(progress_path)?; - let progress_started = std::time::Instant::now(); - let mut write_error = None; - let loaded = if args.backend == RenderBackendMode::StaticVulkan { - load_mission_static_preview_roots_with_progress( - engine, - request, - args.preview_roots, - |phase| { - if write_error.is_none() { - if let Err(err) = - write_load_progress(progress_path, progress_started.elapsed(), phase) - { - write_error = Some(err); - } - } - }, - ) - } else { - load_mission_with_progress(engine, request, |phase| { - if write_error.is_none() { - if let Err(err) = - write_load_progress(progress_path, progress_started.elapsed(), phase) - { - write_error = Some(err); - } - } - }) - } - .map_err(|err| err.to_string())?; - if let Some(err) = write_error { - return Err(err); - } - std::fs::OpenOptions::new() - .append(true) - .open(progress_path) - .and_then(|mut file| { - use std::io::Write; - writeln!( - file, - "Complete\telapsed_ms={}", - progress_started.elapsed().as_millis() - ) - }) - .map_err(|err| format!("{}: {err}", progress_path.display()))?; - return Ok(loaded); - } - if args.backend == RenderBackendMode::StaticVulkan { - if args.preview_roots.get() == 1 { - load_mission_static_preview(engine, request) - } else { - load_mission_static_preview_roots(engine, request, args.preview_roots) - } + if args.preview_roots.get() == 1 { + load_mission_static_preview(engine, request) } else { - load_mission(engine, request) + load_mission_static_preview_roots(engine, request, args.preview_roots) } .map_err(|err| err.to_string()) } @@ -243,11 +114,8 @@ struct StaticPreviewScene { materials: Vec<VulkanStaticMaterial>, camera: VulkanStaticCamera, camera_mode: &'static str, - clip_visible_vertices: usize, mesh_components: usize, terrain_components: usize, - animation_frame: Option<u16>, - material_phase: Option<u16>, } /// Projects the mission terrain plus every MSH component of the explicitly @@ -259,7 +127,7 @@ struct StaticPreviewScene { /// retained on draw ranges but awaits a recovered blend equation; orientation, /// later material phases, animation, lightmaps and gameplay visibility remain /// outside this bridge. -#[allow(clippy::too_many_arguments, clippy::too_many_lines)] + fn static_preview_mesh_and_materials( assets: &MissionAssets, terrain: &TerrainWorld, @@ -366,7 +234,6 @@ fn static_preview_mesh_and_materials( } let camera = legacy_camera.unwrap_or_default(); Ok(StaticPreviewScene { - clip_visible_vertices: static_preview_clip_visible_vertices(&mesh, camera), mesh, materials, camera, @@ -377,51 +244,9 @@ fn static_preview_mesh_and_materials( }, mesh_components, terrain_components: 1, - animation_frame: static_animation_frame, - material_phase: static_material_phase, }) } -/// Counts source vertices inside the Vulkan clip volume for a static preview. -/// -/// The static vertex shader receives row-major D3D7 matrix data as a GLSL -/// column-major `mat4`; its multiplication is consequently equivalent to this -/// row-vector calculation. This remains CPU-only diagnostic evidence and does -/// not change the renderer's clipping rules. -fn static_preview_clip_visible_vertices( - mesh: &VulkanStaticMesh, - camera: VulkanStaticCamera, -) -> usize { - mesh.vertices - .iter() - .filter(|vertex| { - let [x, y, z] = vertex.position; - let matrix = camera.clip_from_world; - let clip_x = x.mul_add( - matrix[0], - y.mul_add(matrix[4], z.mul_add(matrix[8], matrix[12])), - ); - let clip_y = x.mul_add( - matrix[1], - y.mul_add(matrix[5], z.mul_add(matrix[9], matrix[13])), - ); - let clip_z = x.mul_add( - matrix[2], - y.mul_add(matrix[6], z.mul_add(matrix[10], matrix[14])), - ); - let clip_w = x.mul_add( - matrix[3], - y.mul_add(matrix[7], z.mul_add(matrix[11], matrix[15])), - ); - clip_w.is_finite() - && clip_w > 0.0 - && (-clip_w..=clip_w).contains(&clip_x) - && (-clip_w..=clip_w).contains(&clip_y) - && (0.0..=clip_w).contains(&clip_z) - }) - .count() -} - fn static_preview_xy_frame( assets: &MissionAssets, terrain: &TerrainWorld, @@ -518,7 +343,7 @@ fn static_preview_terrain_base_materials( .into_iter() .map(|packed_tag| { let selection = TerrainMaterialLayers::from_packed_tag(packed_tag).land2_selection(); - let selected = load_standalone_wear_material_texture_mip0_rgba8_from_root( + let image = load_standalone_wear_material_texture_mip0_rgba8_from_root( root, &wear_path, selection.material_index, @@ -535,9 +360,9 @@ fn static_preview_terrain_base_materials( materials.push(VulkanStaticMaterial { material_index: preview_selector, texture: VulkanStaticTexture { - width: selected.image.width, - height: selected.image.height, - rgba8: selected.image.rgba8, + width: image.width, + height: image.height, + rgba8: image.rgba8, }, }); Ok((packed_tag, preview_selector)) @@ -664,46 +489,23 @@ fn append_static_preview_component( Ok(()) } -fn prepare_load_progress_path(path: &std::path::Path) -> Result<(), String> { - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).map_err(|err| format!("{}: {err}", parent.display()))?; - } - std::fs::write(path, "Starting\telapsed_ms=0\n") - .map_err(|err| format!("{}: {err}", path.display())) -} - -fn write_load_progress( - path: &std::path::Path, - elapsed: std::time::Duration, - phase: MissionLoadPhase, -) -> Result<(), String> { - use std::io::Write; - - let mut file = std::fs::OpenOptions::new() - .append(true) - .open(path) - .map_err(|err| format!("{}: {err}", path.display()))?; - writeln!(file, "{phase:?}\telapsed_ms={}", elapsed.as_millis()) - .map_err(|err| format!("{}: {err}", path.display())) -} - fn run_static_vulkan_mode( preview: StaticPreviewScene, - preview_roots: usize, target_frames: u64, mission: &str, object_count: usize, readback_out: Option<&std::path::Path>, + enable_validation: bool, ) -> Result<String, String> { let event_loop = EventLoop::new().map_err(|err| format!("winit event loop: {err}"))?; event_loop.set_control_flow(ControlFlow::Poll); let mut app = StaticVulkanApp::new( preview, - preview_roots, target_frames, mission, object_count, readback_out.map(std::path::Path::to_path_buf), + enable_validation, ); if let Err(err) = event_loop.run_app(&mut app) { app.error = Some(format!("winit event loop: {err}")); @@ -716,16 +518,13 @@ struct StaticVulkanApp { materials: Vec<VulkanStaticMaterial>, camera: VulkanStaticCamera, camera_mode: &'static str, - clip_visible_vertices: usize, mesh_components: usize, terrain_components: usize, - animation_frame: Option<u16>, - material_phase: Option<u16>, - preview_roots: usize, target_frames: u64, mission: String, object_count: usize, readback_out: Option<PathBuf>, + enable_validation: bool, window_id: Option<WindowId>, window: Option<Window>, renderer: Option<VulkanSmokeRenderer>, @@ -737,27 +536,24 @@ struct StaticVulkanApp { impl StaticVulkanApp { fn new( preview: StaticPreviewScene, - preview_roots: usize, target_frames: u64, mission: &str, object_count: usize, readback_out: Option<PathBuf>, + enable_validation: bool, ) -> Self { Self { mesh: preview.mesh, materials: preview.materials, camera: preview.camera, camera_mode: preview.camera_mode, - clip_visible_vertices: preview.clip_visible_vertices, mesh_components: preview.mesh_components, terrain_components: preview.terrain_components, - animation_frame: preview.animation_frame, - material_phase: preview.material_phase, - preview_roots, target_frames, mission: mission.to_string(), object_count, readback_out, + enable_validation, window_id: None, window: None, renderer: None, @@ -788,7 +584,9 @@ impl StaticVulkanApp { } }; self.window.take(); - if report.validation.warning_count != 0 || report.validation.error_count != 0 { + if self.enable_validation + && (report.validation.warning_count != 0 || report.validation.error_count != 0) + { self.error = Some(format!( "native Vulkan validation must stay clean (warnings={}, errors={})", report.validation.warning_count, report.validation.error_count @@ -796,6 +594,11 @@ impl StaticVulkanApp { event_loop.exit(); return; } + let validation_status = if self.enable_validation { + "clean" + } else { + "disabled" + }; let readback_path = match (&self.readback_out, &report.readback_artifact) { (Some(path), Some(artifact)) => { if let Some(parent) = path.parent() { @@ -821,27 +624,16 @@ impl StaticVulkanApp { (None, _) => None, }; self.output = Some(format!( - "{{\"report_kind\":\"rendered-static-mission\",\"backend\":\"vulkan-static\",\"camera_mode\":{},\"window\":\"native\",\"mission\":{},\"objects\":{},\"frames\":{},\"preview_roots\":{},\"animation_frame\":{},\"material_phase\":{},\"mesh_components\":{},\"terrain_components\":{},\"clip_visible_vertices\":{},\"material_descriptors\":{},\"swapchain\":[{},{}],\"swapchain_images\":{},\"validation_warnings\":{},\"validation_errors\":{},\"readback_format\":{},\"readback_bytes\":{},\"readback_hash\":{},\"readback_path\":{}}}", - json_string(self.camera_mode), - json_string(&self.mission), - self.object_count, + "rendered mission {}: {} frames, {} objects, {} mesh components, {} terrain components, camera={}, materials={}, validation={}, readback={}", + self.mission, self.frames_presented, - self.preview_roots, - self.animation_frame.map_or_else(|| "null".to_string(), |frame| frame.to_string()), - self.material_phase.map_or_else(|| "null".to_string(), |phase| phase.to_string()), + self.object_count, self.mesh_components, self.terrain_components, - self.clip_visible_vertices, + self.camera_mode, self.materials.len(), - report.renderer_report.swapchain_extent.0, - report.renderer_report.swapchain_extent.1, - report.renderer_report.swapchain_image_count, - report.validation.warning_count, - report.validation.error_count, - report.readback_artifact.as_ref().map_or(0, |artifact| artifact.format), - report.renderer_report.readback_byte_count, - report.renderer_report.readback_fnv1a64, - readback_path.as_deref().map_or_else(|| "null".to_string(), json_string), + validation_status, + readback_path.as_deref().unwrap_or("none"), )); event_loop.exit(); } @@ -890,7 +682,7 @@ impl ApplicationHandler for StaticVulkanApp { native_handles, drawable_extent: (size.width.max(1), size.height.max(1)), render_request: WinitWindow::default_render_request(), - enable_validation: true, + enable_validation: self.enable_validation, mesh: self.mesh.clone(), camera: self.camera, materials: self.materials.clone(), @@ -920,8 +712,7 @@ impl ApplicationHandler for StaticVulkanApp { } match event { WindowEvent::CloseRequested => { - self.error = Some("native Vulkan window closed before completion".to_string()); - event_loop.exit(); + self.complete(event_loop); } WindowEvent::Resized(size) => { if let Some(renderer) = self.renderer.as_mut() { @@ -964,157 +755,12 @@ impl ApplicationHandler for StaticVulkanApp { } } -#[cfg(test)] -fn render_snapshot_commands(snapshot: &WorldSnapshot) -> Result<RenderCommandList, String> { - render_snapshot_commands_with_assets(snapshot, None, None) -} - -fn render_snapshot_commands_with_assets( - snapshot: &WorldSnapshot, - mission_assets: Option<&MissionAssets>, - mission_drafts: Option<&[MissionObjectDraft]>, -) -> Result<RenderCommandList, String> { - let render_snapshot = render_snapshot_with_assets(snapshot, mission_assets, mission_drafts); - build_commands(&render_snapshot, RenderProfile::default()).map_err(|err| err.to_string()) -} - -fn render_snapshot_with_assets( - snapshot: &WorldSnapshot, - mission_assets: Option<&MissionAssets>, - mission_drafts: Option<&[MissionObjectDraft]>, -) -> RenderSnapshot { - let mut draws = Vec::with_capacity(snapshot.objects.len()); - for (index, handle) in snapshot.objects.iter().enumerate() { - let visuals = mission_assets.map_or(&[][..], |assets| assets.visuals_for_object(index)); - let visual_count = visuals.len().max(1); - for visual_index in 0..visual_count { - let prepared = mission_assets.and_then(|assets| { - visuals - .get(visual_index) - .and_then(|visual_id| assets.visual_by_id(*visual_id)) - }); - let mesh = prepared.map_or_else( - || GpuMeshId(u64::from(handle.slot) + 1), - |visual| GpuMeshId(visual.id.raw()), - ); - let material = prepared - .and_then(PreparedVisual::primary_material_id) - .map_or(GpuMaterialId(1), |material_id| { - GpuMaterialId(material_id.raw()) - }); - let stable_order = if visual_count == 1 { - u64::from(handle.slot) - } else { - u64::from(handle.slot) - .wrapping_mul(1_000_003) - .wrapping_add(u64::try_from(visual_index).unwrap_or(u64::MAX)) - }; - let draw_id = snapshot - .tick - .0 - .wrapping_mul(1_000_003) - .wrapping_add(stable_order); - draws.push(RenderSnapshotDraw { - id: DrawId(draw_id), - phase: RenderPhase::Opaque, - object_id: mission_drafts - .and_then(|drafts| drafts.get(index)) - .and_then(|draft| draft.original_id), - mesh, - material_slots: vec![material], - material_index: 0, - pipeline_state: fparkan_render::LegacyPipelineState::default(), - transform: snapshot_transform(snapshot, *handle).unwrap_or_else(|| { - mission_drafts - .and_then(|drafts| drafts.get(index)) - .map_or_else( - || identity_transform(index_to_f32(index)), - mission_position_scale_transform, - ) - }), - range: IndexRange { start: 0, count: 3 }, - stable_order, - }); - } - } - RenderSnapshot { - camera: CameraSnapshot::default(), - draws, - } -} - -fn snapshot_transform( - snapshot: &WorldSnapshot, - handle: fparkan_world::ObjectHandle, -) -> Option<[f32; 16]> { - snapshot - .transforms - .iter() - .find_map(|(candidate, transform)| (*candidate == handle).then_some(*transform)) - .map(transform_state_position_scale_transform) -} - -fn transform_state_position_scale_transform(transform: TransformState) -> [f32; 16] { - let position = transform.position.map(f32::from_bits); - let scale = transform.scale.map(f32::from_bits); - [ - scale[0], - 0.0, - 0.0, - position[0], - 0.0, - scale[1], - 0.0, - position[1], - 0.0, - 0.0, - scale[2], - position[2], - 0.0, - 0.0, - 0.0, - 1.0, - ] -} - -fn mission_position_scale_transform(draft: &MissionObjectDraft) -> [f32; 16] { - [ - draft.scale[0], - 0.0, - 0.0, - draft.position[0], - 0.0, - draft.scale[1], - 0.0, - draft.position[1], - 0.0, - 0.0, - draft.scale[2], - draft.position[2], - 0.0, - 0.0, - 0.0, - 1.0, - ] -} - -fn identity_transform(x: f32) -> [f32; 16] { - [ - 1.0, 0.0, 0.0, x, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, - ] -} - -fn index_to_f32(index: usize) -> f32 { - u16::try_from(index).map_or(f32::from(u16::MAX), f32::from) -} - #[derive(Clone, Debug, Eq, PartialEq)] struct Args { root: PathBuf, mission: String, frames: u64, - backend: RenderBackendMode, - load_progress: Option<PathBuf>, + validation: bool, readback_out: Option<PathBuf>, preview_roots: NonZeroUsize, legacy_camera_capture: Option<PathBuf>, @@ -1122,20 +768,12 @@ struct Args { static_material_phase: Option<u16>, } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum RenderBackendMode { - Planning, - StaticVulkan, -} - impl Args { - #[allow(clippy::too_many_lines)] fn parse(args: &[String]) -> Result<Self, String> { let mut root = None; let mut mission = None; let mut frames = 1; - let mut backend = RenderBackendMode::Planning; - let mut load_progress = None; + let mut validation = false; let mut readback_out = None; // A native static-Vulkan invocation is the usable mission preview, so // its default scope covers every root. `PreviewRoots` is clamped by the @@ -1169,23 +807,8 @@ impl Args { .parse() .map_err(|_| "--frames must be an integer".to_string())?; } - "--backend" => { - backend = match iter - .next() - .ok_or_else(|| "--backend requires a value".to_string())? - .as_str() - { - "planning" => RenderBackendMode::Planning, - "static-vulkan" => RenderBackendMode::StaticVulkan, - _ => return Err("--backend must be planning or static-vulkan".to_string()), - }; - } - "--load-progress" => { - load_progress = Some( - iter.next() - .map(PathBuf::from) - .ok_or_else(|| "--load-progress requires a path".to_string())?, - ); + "--validation" => { + validation = true; } "--readback-out" => { readback_out = Some( @@ -1235,24 +858,11 @@ impl Args { if frames == 0 { return Err("--frames must be greater than zero".to_string()); } - if legacy_camera_capture.is_some() && backend != RenderBackendMode::StaticVulkan { - return Err("--legacy-camera-capture requires --backend static-vulkan".to_string()); - } - if readback_out.is_some() && backend != RenderBackendMode::StaticVulkan { - return Err("--readback-out requires --backend static-vulkan".to_string()); - } - if static_animation_frame.is_some() && backend != RenderBackendMode::StaticVulkan { - return Err("--static-animation-frame requires --backend static-vulkan".to_string()); - } - if static_material_phase.is_some() && backend != RenderBackendMode::StaticVulkan { - return Err("--static-material-phase requires --backend static-vulkan".to_string()); - } Ok(Self { root, mission, frames, - backend, - load_progress, + validation, readback_out, preview_roots, legacy_camera_capture, @@ -1339,87 +949,19 @@ fn decode_legacy_camera_capture_json(bytes: &[u8]) -> Result<String, String> { } } -fn json_string(value: &str) -> String { - let mut out = String::with_capacity(value.len() + 2); - out.push('"'); - for ch in value.chars() { - match ch { - '"' => out.push_str("\\\""), - '\\' => out.push_str("\\\\"), - '\n' => out.push_str("\\n"), - '\r' => out.push_str("\\r"), - '\t' => out.push_str("\\t"), - c if c.is_control() => { - use std::fmt::Write as _; - let _ = write!(out, "\\u{:04x}", c as u32); - } - c => out.push(c), - } - } - out.push('"'); - out -} - -fn json_hash(hash: &[u8; 32]) -> String { - let mut out = String::from("\""); - for byte in hash { - use std::fmt::Write as _; - let _ = write!(out, "{byte:02x}"); - } - out.push('"'); - out -} - fn usage() -> String { - "usage: fparkan-game --root <path> --mission <path> [--frames <n>] [--backend <planning|static-vulkan>] [--preview-roots <non-zero n>] [--legacy-camera-capture <path>] [--static-animation-frame <u16>] [--static-material-phase <u16>] [--readback-out <path>] [--load-progress <path>]".to_string() + "usage: fparkan-game --root <path> --mission <path> [--frames <n>] [--validation] [--preview-roots <non-zero n>] [--legacy-camera-capture <path>] [--static-animation-frame <u16>] [--static-material-phase <u16>] [--readback-out <path>]".to_string() } #[cfg(test)] mod tests { use super::*; - use fparkan_assets::AssetId; - use fparkan_world::{ObjectHandle, StateHash, Tick}; - use std::path::Path; fn strings(values: &[&str]) -> Vec<String> { values.iter().map(|value| (*value).to_string()).collect() } #[test] - fn static_preview_clip_diagnostic_matches_vulkan_clip_bounds() { - let mesh = VulkanStaticMesh { - vertices: vec![ - fparkan_render_vulkan::VulkanStaticVertex { - position: [0.0, 0.0, 0.0], - color: [0.0; 3], - uv: [0.0; 2], - }, - fparkan_render_vulkan::VulkanStaticVertex { - position: [1.0, -1.0, 1.0], - color: [0.0; 3], - uv: [0.0; 2], - }, - fparkan_render_vulkan::VulkanStaticVertex { - position: [1.01, 0.0, 0.0], - color: [0.0; 3], - uv: [0.0; 2], - }, - fparkan_render_vulkan::VulkanStaticVertex { - position: [0.0, 0.0, -0.01], - color: [0.0; 3], - uv: [0.0; 2], - }, - ], - indices: Vec::new(), - draw_ranges: Vec::new(), - }; - assert_eq!( - static_preview_clip_visible_vertices(&mesh, VulkanStaticCamera::default()), - 2 - ); - } - - #[test] fn parses_required_args() { assert_eq!( Args::parse(&strings(&[ @@ -1434,8 +976,7 @@ mod tests { root: PathBuf::from("testdata/IS"), mission: "MISSIONS/Autodemo.00/data.tma".to_string(), frames: 3, - backend: RenderBackendMode::Planning, - load_progress: None, + validation: false, readback_out: None, preview_roots: NonZeroUsize::MAX, legacy_camera_capture: None, @@ -1446,29 +987,16 @@ mod tests { } #[test] - fn parses_static_vulkan_backend() { - assert_eq!( - Args::parse(&strings(&[ - "--root", - "testdata/IS", - "--mission", - "MISSIONS/Autodemo.00/data.tma", - "--backend", - "static-vulkan", - ])), - Ok(Args { - root: PathBuf::from("testdata/IS"), - mission: "MISSIONS/Autodemo.00/data.tma".to_string(), - frames: 1, - backend: RenderBackendMode::StaticVulkan, - load_progress: None, - readback_out: None, - preview_roots: NonZeroUsize::MAX, - legacy_camera_capture: None, - static_animation_frame: None, - static_material_phase: None, - }) - ); + fn parses_validation_flag() { + let parsed = Args::parse(&strings(&[ + "--root", + "testdata/IS", + "--mission", + "MISSIONS/Autodemo.00/data.tma", + "--validation", + ])) + .expect("valid validation flag"); + assert!(parsed.validation); } #[test] @@ -1478,8 +1006,6 @@ mod tests { "testdata/IS", "--mission", "MISSIONS/Autodemo.00/data.tma", - "--backend", - "static-vulkan", "--preview-roots", "2", ])) @@ -1491,26 +1017,12 @@ mod tests { } #[test] - fn readback_output_requires_static_vulkan_and_is_retained() { - let common = [ - "--root", - "testdata/IS", - "--mission", - "MISSIONS/Autodemo.00/data.tma", - "--readback-out", - "target/frame.raw", - ]; - assert_eq!( - Args::parse(&strings(&common)), - Err("--readback-out requires --backend static-vulkan".to_string()) - ); + fn parses_readback_output() { let parsed = Args::parse(&strings(&[ "--root", "testdata/IS", "--mission", "MISSIONS/Autodemo.00/data.tma", - "--backend", - "static-vulkan", "--readback-out", "target/frame.raw", ])) @@ -1519,26 +1031,12 @@ mod tests { } #[test] - fn static_animation_frame_requires_static_vulkan_and_is_retained() { - let common = [ - "--root", - "testdata/IS", - "--mission", - "MISSIONS/Autodemo.00/data.tma", - "--static-animation-frame", - "12", - ]; - assert_eq!( - Args::parse(&strings(&common)), - Err("--static-animation-frame requires --backend static-vulkan".to_string()) - ); + fn parses_static_animation_frame() { let parsed = Args::parse(&strings(&[ "--root", "testdata/IS", "--mission", "MISSIONS/Autodemo.00/data.tma", - "--backend", - "static-vulkan", "--static-animation-frame", "12", ])) @@ -1547,26 +1045,12 @@ mod tests { } #[test] - fn static_material_phase_requires_static_vulkan_and_is_retained() { - let common = [ - "--root", - "testdata/IS", - "--mission", - "MISSIONS/Autodemo.00/data.tma", - "--static-material-phase", - "2", - ]; - assert_eq!( - Args::parse(&strings(&common)), - Err("--static-material-phase requires --backend static-vulkan".to_string()) - ); + fn parses_static_material_phase() { let parsed = Args::parse(&strings(&[ "--root", "testdata/IS", "--mission", "MISSIONS/Autodemo.00/data.tma", - "--backend", - "static-vulkan", "--static-material-phase", "2", ])) @@ -1591,52 +1075,12 @@ mod tests { } #[test] - fn parses_load_progress_path() { - assert_eq!( - Args::parse(&strings(&[ - "--root", - "testdata/IS", - "--mission", - "MISSIONS/Autodemo.00/data.tma", - "--load-progress", - "target/probe.txt", - ])), - Ok(Args { - root: PathBuf::from("testdata/IS"), - mission: "MISSIONS/Autodemo.00/data.tma".to_string(), - frames: 1, - backend: RenderBackendMode::Planning, - load_progress: Some(PathBuf::from("target/probe.txt")), - readback_out: None, - preview_roots: NonZeroUsize::MAX, - legacy_camera_capture: None, - static_animation_frame: None, - static_material_phase: None, - }) - ); - } - - #[test] - fn legacy_camera_capture_requires_static_vulkan_and_is_retained() { - let common = [ - "--root", - "testdata/IS", - "--mission", - "MISSIONS/Autodemo.00/data.tma", - "--legacy-camera-capture", - "target/camera.json", - ]; - assert_eq!( - Args::parse(&strings(&common)), - Err("--legacy-camera-capture requires --backend static-vulkan".to_string()) - ); + fn parses_legacy_camera_capture_path() { let valid = Args::parse(&strings(&[ "--root", "testdata/IS", "--mission", "MISSIONS/Autodemo.00/data.tma", - "--backend", - "static-vulkan", "--legacy-camera-capture", "target/camera.json", ])) @@ -1691,38 +1135,6 @@ mod tests { } #[test] - fn load_progress_retains_prior_milestones() -> Result<(), String> { - let path = - std::env::temp_dir().join(format!("fparkan-game-progress-{}.txt", std::process::id())); - prepare_load_progress_path(&path)?; - write_load_progress( - &path, - std::time::Duration::from_millis(12), - MissionLoadPhase::GraphVisualMaterials, - )?; - write_load_progress( - &path, - std::time::Duration::from_millis(34), - MissionLoadPhase::GraphVisualMaterialRequests { - request_count: 64, - graph_node_count: 128, - graph_edge_count: 192, - wear_cache_entries: 8, - material_cache_entries: 16, - texture_cache_entries: 32, - }, - )?; - let progress = std::fs::read_to_string(&path).map_err(|err| err.to_string())?; - std::fs::remove_file(&path).map_err(|err| err.to_string())?; - - assert_eq!( - progress, - "Starting\telapsed_ms=0\nGraphVisualMaterials\telapsed_ms=12\nGraphVisualMaterialRequests { request_count: 64, graph_node_count: 128, graph_edge_count: 192, wear_cache_entries: 8, material_cache_entries: 16, texture_cache_entries: 32 }\telapsed_ms=34\n" - ); - Ok(()) - } - - #[test] fn static_preview_component_merge_offsets_indices_and_remaps_local_selectors( ) -> Result<(), String> { let mut merged = VulkanStaticMesh { @@ -1773,267 +1185,4 @@ mod tests { assert_eq!(merged.indices, vec![65_536, 65_537, 65_538]); Ok(()) } - - #[test] - fn render_commands_follow_snapshot_order() -> Result<(), String> { - let snapshot = WorldSnapshot { - tick: Tick(7), - objects: vec![ - ObjectHandle { - generation: 1, - slot: 2, - }, - ObjectHandle { - generation: 1, - slot: 5, - }, - ], - transforms: Vec::new(), - events: Vec::new(), - hash: StateHash([0; 32]), - }; - - let commands = render_snapshot_commands(&snapshot)?; - - assert_eq!(commands.commands.len(), 4); - assert!(matches!( - commands.commands[0], - fparkan_render::RenderCommand::BeginFrame - )); - assert!(matches!( - commands.commands[3], - fparkan_render::RenderCommand::EndFrame - )); - let fparkan_render::RenderCommand::Draw(first) = &commands.commands[1] else { - return Err("expected draw".to_string()); - }; - assert_eq!(first.mesh, GpuMeshId(3)); - assert_eq!(first.stable_order, 2); - Ok(()) - } - - #[test] - fn render_snapshot_retains_every_prepared_visual_for_an_object() -> Result<(), String> { - let snapshot = WorldSnapshot { - tick: Tick(3), - objects: vec![ObjectHandle { - generation: 1, - slot: 7, - }], - transforms: Vec::new(), - events: Vec::new(), - hash: StateHash([0; 32]), - }; - let first = prepared_visual(101, 201); - let second = prepared_visual(102, 202); - let assets = MissionAssets { - visuals: vec![first, second], - object_visuals: vec![vec![AssetId::new(101), AssetId::new(102)]], - ..MissionAssets::default() - }; - let commands = render_snapshot_commands_with_assets(&snapshot, Some(&assets), None)?; - let draws: Vec<_> = commands - .commands - .iter() - .filter_map(|command| match command { - RenderCommand::Draw(draw) => Some(draw), - _ => None, - }) - .collect(); - assert_eq!(draws.len(), 2); - assert_eq!(draws[0].mesh, GpuMeshId(101)); - assert_eq!(draws[1].mesh, GpuMeshId(102)); - assert_eq!(draws[0].material, GpuMaterialId(201)); - assert_eq!(draws[1].material, GpuMaterialId(202)); - assert_ne!(draws[0].stable_order, draws[1].stable_order); - Ok(()) - } - - #[test] - fn render_snapshot_uses_mission_position_and_scale_without_interpreting_orientation() { - let draft = MissionObjectDraft { - original_id: None, - resource_name_raw: Vec::new(), - identity_or_clan_raw: 0, - position: [10.0, 20.0, 30.0], - orientation_raw: [1.0, 2.0, 3.0], - scale: [2.0, 3.0, 4.0], - visual_ids: Vec::new(), - properties: Vec::new(), - unit_components: Vec::new(), - }; - assert_eq!( - mission_position_scale_transform(&draft), - [2.0, 0.0, 0.0, 10.0, 0.0, 3.0, 0.0, 20.0, 0.0, 0.0, 4.0, 30.0, 0.0, 0.0, 0.0, 1.0,] - ); - } - - #[test] - fn render_snapshot_prefers_world_transform_over_mission_draft() -> Result<(), String> { - let handle = ObjectHandle { - generation: 1, - slot: 0, - }; - let snapshot = WorldSnapshot { - tick: Tick(1), - objects: vec![handle], - transforms: vec![( - handle, - TransformState { - position: [ - 100.0_f32.to_bits(), - 200.0_f32.to_bits(), - 300.0_f32.to_bits(), - ], - orientation: [0; 3], - scale: [4.0_f32.to_bits(), 5.0_f32.to_bits(), 6.0_f32.to_bits()], - }, - )], - events: Vec::new(), - hash: StateHash([0; 32]), - }; - let drafts = vec![MissionObjectDraft { - original_id: None, - resource_name_raw: Vec::new(), - identity_or_clan_raw: 0, - position: [1.0; 3], - orientation_raw: [0.0; 3], - scale: [1.0; 3], - visual_ids: Vec::new(), - properties: Vec::new(), - unit_components: Vec::new(), - }]; - - let commands = render_snapshot_commands_with_assets(&snapshot, None, Some(&drafts))?; - let RenderCommand::Draw(draw) = &commands.commands[1] else { - return Err("expected draw".to_string()); - }; - assert_eq!( - draw.transform, - [ - 4.0, 0.0, 0.0, 100.0, 0.0, 5.0, 0.0, 200.0, 0.0, 0.0, 6.0, 300.0, 0.0, 0.0, 0.0, - 1.0 - ] - ); - Ok(()) - } - - #[test] - fn render_snapshot_preserves_mission_original_object_id() -> Result<(), String> { - let snapshot = WorldSnapshot { - tick: Tick(1), - objects: vec![ObjectHandle { - generation: 1, - slot: 0, - }], - transforms: Vec::new(), - events: Vec::new(), - hash: StateHash([0; 32]), - }; - let drafts = vec![MissionObjectDraft { - original_id: Some(OriginalObjectId(42)), - resource_name_raw: Vec::new(), - identity_or_clan_raw: 0, - position: [0.0; 3], - orientation_raw: [0.0; 3], - scale: [1.0; 3], - visual_ids: Vec::new(), - properties: Vec::new(), - unit_components: Vec::new(), - }]; - - let commands = render_snapshot_commands_with_assets(&snapshot, None, Some(&drafts))?; - let RenderCommand::Draw(draw) = &commands.commands[1] else { - return Err("expected draw".to_string()); - }; - assert_eq!(draw.object_id, Some(OriginalObjectId(42))); - Ok(()) - } - - fn prepared_visual(id: u64, material: u64) -> PreparedVisual { - PreparedVisual { - id: AssetId::new(id), - mesh: None, - model_id: None, - wear_id: None, - model_nodes: 0, - model_slots: 0, - model_batches: 0, - material_count: 1, - material_ids: vec![AssetId::new(material)], - texture_ids: Vec::new(), - lightmap_ids: Vec::new(), - texture_count: 0, - lightmap_count: 0, - } - } - - #[test] - #[ignore = "requires licensed corpus"] - fn selected_is_and_is2_missions_produce_approved_render_captures() { - for case in [ - RenderCase { - root: "IS", - mission: "MISSIONS/CAMPAIGN/CAMPAIGN.00/Mission.01/data.tma", - expected: "{\"report_kind\":\"render-planning\",\"backend\":\"vulkan-planning\",\"window\":\"synthetic\",\"mission\":\"MISSIONS/CAMPAIGN/CAMPAIGN.00/Mission.01/data.tma\",\"objects\":33,\"frames\":1,\"tick\":1,\"draws\":33,\"submission_plans\":1,\"last_command_capture_bytes\":2008,\"hash\":\"ca17cc76e55c45e83c1c9c1c088e84bf1a698be91a7730943210fe27596af841\"}", - }, - RenderCase { - root: "IS2", - mission: "MISSIONS/Campaign/CAMPAIGN.00/Mission.02/data.tma", - expected: "{\"report_kind\":\"render-planning\",\"backend\":\"vulkan-planning\",\"window\":\"synthetic\",\"mission\":\"MISSIONS/Campaign/CAMPAIGN.00/Mission.02/data.tma\",\"objects\":10,\"frames\":1,\"tick\":1,\"draws\":10,\"submission_plans\":1,\"last_command_capture_bytes\":605,\"hash\":\"5d720b3ab690076a398a79a404850bbeaee2e33811b5bb570ec8a96d4a7a2fc4\"}", - }, - ] { - assert_eq!( - run(&render_args(&licensed_root(case.root), case.mission)), - Ok(case.expected.to_string()) - ); - } - } - - #[test] - fn json_hash_is_hex() { - let mut hash = [0; 32]; - hash[0] = 0xab; - hash[31] = 0xcd; - - assert_eq!( - json_hash(&hash), - "\"ab000000000000000000000000000000000000000000000000000000000000cd\"" - ); - } - - #[derive(Clone, Copy)] - struct RenderCase { - root: &'static str, - mission: &'static str, - expected: &'static str, - } - - fn render_args(root: &Path, mission: &str) -> Vec<String> { - vec![ - "--root".to_string(), - root.to_str().expect("utf8 root").to_string(), - "--mission".to_string(), - mission.to_string(), - "--frames".to_string(), - "1".to_string(), - ] - } - - fn licensed_root(name: &str) -> PathBuf { - let variable = match name { - "IS" => "FPARKAN_CORPUS_PART1_ROOT", - "IS2" => "FPARKAN_CORPUS_PART2_ROOT", - _ => panic!("unknown licensed corpus part: {name}"), - }; - let root = std::env::var_os(variable) - .map(PathBuf::from) - .unwrap_or_else(|| panic!("{variable} is required for licensed corpus tests")); - assert!( - root.is_dir(), - "licensed corpus root is missing: {}", - root.display() - ); - root - } } |
