diff --git a/crates/skate-game/src/retail_render.rs b/crates/skate-game/src/retail_render.rs index d263c9ea..2b969e62 100644 --- a/crates/skate-game/src/retail_render.rs +++ b/crates/skate-game/src/retail_render.rs @@ -22,7 +22,6 @@ fn retail_shader(path: &str) -> ShaderRef { impl Plugin for RetailRenderPlugin { fn build(&self, app: &mut App) { shadow::install(app); - shadow_visibility::install(app); exposure::install(app); if std::env::var_os("SKATE_WEATHERING_COMPARE").is_some_and(|v| v == "1") { app.add_systems(Update, compare_weathering); @@ -41,9 +40,10 @@ impl Plugin for RetailRenderPlugin { MaterialPlugin::::default(), )); } - fn finish(&self, app: &mut App) { shadow_visibility::finish(app); } } +// Retained for headless experiments; production uses Bevy directional visibility. +#[cfg(test)] #[path = "retail_shadow_visibility.rs"] pub(crate) mod shadow_visibility; diff --git a/crates/skate-game/src/retail_shadow_visibility_tests.rs b/crates/skate-game/src/retail_shadow_visibility_tests.rs index fbee638d..f3546850 100644 --- a/crates/skate-game/src/retail_shadow_visibility_tests.rs +++ b/crates/skate-game/src/retail_shadow_visibility_tests.rs @@ -150,7 +150,21 @@ fn indexed_system_matches_upstream_across_changes_and_removal() { } original.run(&mut world); let expected = read(&world); + for &entity in &entities { + if let Some(mut visibility) = world.get_mut::(entity) { + *visibility = ViewVisibility::HIDDEN; + } + } indexed.run(&mut world); + for &entity in &entities { + if let Some(visibility) = world.get::(entity) { + assert_eq!( + visibility.get(), + expected.iter().any(|list| list.contains(&entity)), + "visibility marking at step {step}" + ); + } + } assert_eq!(read(&world), expected, "step {step}"); } for entity in entities { @@ -178,6 +192,7 @@ pub(super) fn benchmark_bounds(bounds: Vec) -> serde_json::Value { let mut world = World::new(); let mut index = Index::default(); let count = bounds.len(); + let system_bounds = bounds.clone(); let start = std::time::Instant::now(); index.rebuild( bounds @@ -247,5 +262,84 @@ pub(super) fn benchmark_bounds(bounds: Vec) -> serde_json::Value { } } let indexed_ms = start.elapsed().as_secs_f64() * 1000.; - serde_json::json!({"static_batches":count,"queries":repetitions*frusta.len(),"flat_ms":flat_ms,"indexed_ms":indexed_ms,"build_ms":build_ms,"visible_sets_equal":true,"note":"real University material bounds; representative shadow frusta, single-thread query kernel only, not whole-frame FPS"}) + serde_json::json!({"full_system":benchmark_systems(system_bounds),"static_batches":count,"queries":repetitions*frusta.len(),"flat_ms":flat_ms,"indexed_ms":indexed_ms,"build_ms":build_ms,"visible_sets_equal":true,"note":"real University material bounds; representative shadow frusta, single-thread query kernel only, not whole-frame FPS"}) +} + +fn benchmark_systems(bounds: Vec) -> serde_json::Value { + bevy::tasks::ComputeTaskPool::get_or_init(Default::default); + let mut world = World::new(); + world.init_resource::(); + let view = world.spawn_empty().id(); + let center = Vec3::new(330., 133., -710.); + let direction = Vec3::new(-0.60385966, 0.73098797, 0.31782088); + let frusta: Vec<_> = [5., 10., 30., 100.] + .into_iter() + .map(|radius| { + Frustum::from_clip_from_world( + &(Mat4::orthographic_rh(-radius, radius, -radius, radius, 0.1, 400.) + * Mat4::look_at_rh(center + direction * 200., center, Vec3::Y)), + ) + }) + .collect(); + for layer in [0, 28] { + let mut cascades = CascadesFrusta::default(); + cascades.frusta.insert(view, frusta.clone()); + let light = world + .spawn(( + DirectionalLight { + shadows_enabled: true, + ..default() + }, + cascades, + CascadesVisibleEntities::default(), + RenderLayers::layer(layer), + )) + .id(); + world + .get_mut::(light) + .unwrap() + .set_visible(); + } + for aabb in bounds { + world.spawn(( + StaticShadowCaster, + Mesh3d::default(), + aabb, + GlobalTransform::IDENTITY, + InheritedVisibility::VISIBLE, + )); + } + // A second view has overlapping cascades, deliberately exercising duplicate + // visibility publication, which the old query-only benchmark omitted. + let second_view = world.spawn_empty().id(); + for mut cascades in world.query::<&mut CascadesFrusta>().iter_mut(&mut world) { + cascades.frusta.insert(second_view, frusta.clone()); + } + let mut old = bevy::ecs::schedule::Schedule::default(); + old.add_systems((refresh, check_indexed).chain()); + let mut upstream = bevy::ecs::schedule::Schedule::default(); + upstream.add_systems(bevy::light::check_dir_light_mesh_visibility); + old.run(&mut world); + upstream.run(&mut world); + let mut old_times = vec![]; + let mut upstream_times = vec![]; + for round in 0..5 { + let mut run = |schedule: &mut bevy::ecs::schedule::Schedule| { + let start = std::time::Instant::now(); + for _ in 0..200 { + schedule.run(&mut world); + } + start.elapsed().as_secs_f64() * 1000. / 200. + }; + if round % 2 == 0 { + old_times.push(run(&mut old)); + upstream_times.push(run(&mut upstream)); + } else { + upstream_times.push(run(&mut upstream)); + old_times.push(run(&mut old)); + } + } + old_times.sort_by(f64::total_cmp); + upstream_times.sort_by(f64::total_cmp); + serde_json::json!({"v9_ms_per_update":old_times[2],"upstream_ms_per_update":upstream_times[2],"note":"complete headless schedules including refresh and deferred writes; real map bounds, fixed representative frusta, two overlapping views, not gameplay FPS"}) } diff --git a/crates/skate-game/src/skate_world.rs b/crates/skate-game/src/skate_world.rs index 7c86dbb9..ce8c6d75 100644 --- a/crates/skate-game/src/skate_world.rs +++ b/crates/skate-game/src/skate_world.rs @@ -700,7 +700,7 @@ pub(crate) fn spawn( } if let Some(material) = retail { let material = retail_materials.add(material); - commands.spawn((crate::retail_render::shadow_visibility::StaticShadowCaster, Name::new(m.name.clone()), Mesh3d(meshes.add(mesh)), MeshMaterial3d(material), Transform::default())); + commands.spawn((Name::new(m.name.clone()), Mesh3d(meshes.add(mesh)), MeshMaterial3d(material), Transform::default())); continue; } // Vertex colours above carry retail decal coordinates, never PBR tint. diff --git a/docs/cpu-followup-optimizations.md b/docs/cpu-followup-optimizations.md index aeee8b0e..6bf73905 100644 --- a/docs/cpu-followup-optimizations.md +++ b/docs/cpu-followup-optimizations.md @@ -83,3 +83,39 @@ system-only DLL imports were verified without running gameplay. EXE SHA-256: v8 for comparison. The new capture is armed until F9 and records 20 seconds. Existing compiler warnings remain. Runtime appearance and FPS acceptance are pending the user's test; the agent's GPU runs were isolated headless tests only. + + +## v10: restore the measured shadow baseline + +The completed user captures `university-cpu-bindings-v8-8702-22955.json` and +`university-cpu-followup-v9-5644-32244.json` each cover 20 seconds without dropped +trace events. Median frame time improved from 4.578 to 3.976 ms, but mean frame +time increased from 4.601 to 4.806 ms and p99 from 7.775 to 21.713 ms. These are +not matched camera routes; neither the aggregate improvement nor the hitches +can be attributed conclusively to a particular change. + +Directional shadow system plus deferred-command time increased from approximately +0.525 + 0.036 = 0.561 ms/frame in v8 to 0.573 + 0.243 = 0.816 ms/frame in v9. +The indexed implementation repeats visibility publication across cascade lists. +A candidate using a reused entity set and a bulk mutable query passed visibility +parity tests but did not improve a complete headless schedule benchmark: 0.0610 +versus 0.0590 ms/update for v9. That candidate was discarded. + +The same fixture measured upstream at 0.1410 ms/update, which disagrees with the +relative result in gameplay. It uses actual University bounds but fixed frusta, +two overlapping views, no moving casters and an otherwise idle ECS schedule. +This makes it useful for detecting local overhead, not choosing the fastest +production path or predicting FPS. The inventory now includes full schedule +measurements, including deferred writes, alongside the older query-only kernel. + +v10 restores upstream Bevy directional shadow visibility by removing the custom +system installation/replacement and the retail entity marker. The index remains +compiled only for headless tests and experiments. Material resource reuse, +controller metadata caching and the v8 bindless shader fix remain active. +Shadow geometry, quality and distance are unchanged. This is a conservative +return to the earlier measured path, not a claim that it resolves every hitch. + +Visibility parity tests now also reset and verify per-entity ViewVisibility after +each lifecycle change, in addition to comparing cascade membership. The private +v10 launcher is `TRACE-UNIVERSITY-SHADOW-FIX.bat`; v8 and v9 remain available. +A matched user capture is required to determine the actual frame-time effect.