diff options
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 - } } |
