Centralize world and viewport game loops
This commit is contained in:
@@ -159,12 +159,27 @@ async fn live_primary_map_name_reports_varregion_dimensions() -> Result<(), Box<
|
||||
async fn live_bevy_capture_renders_in_front_of_avatar() -> Result<(), Box<dyn Error>> {
|
||||
let environment = live_environment()?;
|
||||
let renderer_owner = LibremetaverseClientOwner::new()?;
|
||||
let (mut renderer_session, renderer_cancel) =
|
||||
login(&renderer_owner, &environment, "GRID_USER", "GRID_PASSWORD").await?;
|
||||
let limits = VisionLimits {
|
||||
minimum_interval: Duration::ZERO,
|
||||
..VisionLimits::default()
|
||||
};
|
||||
let (_world_events, world_receiver) =
|
||||
metacrate_game_loop::LibreMetaverseWorldEvents::subscribe_visible(
|
||||
renderer_owner.client(),
|
||||
renderer_owner.agent(),
|
||||
limits.max_distance_meters,
|
||||
4_096,
|
||||
)
|
||||
.ok_or("live world event subscription failed")?;
|
||||
let world_loop = metacrate_game_loop::start(
|
||||
metacrate_game_loop::LoopConfig::default(),
|
||||
world_receiver,
|
||||
(),
|
||||
None,
|
||||
)
|
||||
.ok_or("live world loop failed to start")?;
|
||||
let (mut renderer_session, renderer_cancel) =
|
||||
login(&renderer_owner, &environment, "GRID_USER", "GRID_PASSWORD").await?;
|
||||
let scene_source = Arc::new(LibremetaverseSceneSource::new(&renderer_owner, limits));
|
||||
let prefetch_started = Instant::now();
|
||||
scene_source.start_prefetch();
|
||||
@@ -183,6 +198,27 @@ async fn live_bevy_capture_renders_in_front_of_avatar() -> Result<(), Box<dyn Er
|
||||
renderer_simulator.name, region_size.0, region_size.1
|
||||
);
|
||||
wait_scene_settled(&renderer_owner).await?;
|
||||
tokio::time::timeout(Duration::from_secs(10), async {
|
||||
while world_loop
|
||||
.latest_world()
|
||||
.is_none_or(|world| world.value.region.is_none())
|
||||
{
|
||||
tokio::time::sleep(Duration::from_millis(20)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|_| "central world state did not become ready")?;
|
||||
let world = world_loop
|
||||
.latest_world()
|
||||
.ok_or("central world snapshot disappeared")?;
|
||||
println!(
|
||||
"LIVE_WORLD_STATE=sequence:{},primitives:{},avatars:{},terrain_patches:{},dropped_events:{}",
|
||||
world.sequence,
|
||||
world.value.primitives.len(),
|
||||
world.value.avatars.len(),
|
||||
world.value.terrain.len(),
|
||||
world_loop.dropped_world_events(),
|
||||
);
|
||||
let view_forward = Vector3::mul_with_vector3_quaternion(
|
||||
Vector3::unit_x(),
|
||||
renderer_owner.agent().sim_rotation(),
|
||||
@@ -273,6 +309,31 @@ async fn live_bevy_capture_renders_in_front_of_avatar() -> Result<(), Box<dyn Er
|
||||
}
|
||||
}
|
||||
let capture = capture.ok_or("live renderer produced no frame")?;
|
||||
let viewport_before = vision
|
||||
.viewport_stats()
|
||||
.ok_or("viewport loop did not expose statistics")?;
|
||||
let soak_started = Instant::now();
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
let soak_elapsed = soak_started.elapsed();
|
||||
let viewport_after = vision
|
||||
.viewport_stats()
|
||||
.ok_or("viewport loop stopped exposing statistics")?;
|
||||
let soak_frames = viewport_after
|
||||
.completed_frames
|
||||
.saturating_sub(viewport_before.completed_frames);
|
||||
let soak_missed = viewport_after
|
||||
.missed_frame_deadlines
|
||||
.saturating_sub(viewport_before.missed_frame_deadlines);
|
||||
let soak_fps = f64::from(u32::try_from(soak_frames)?) / soak_elapsed.as_secs_f64();
|
||||
println!(
|
||||
"LIVE_VIEWPORT_SOAK=frames:{soak_frames},elapsed_ms:{},fps:{soak_fps:.2},missed_deadlines:{soak_missed},render_errors:{},last_render_ms:{}",
|
||||
soak_elapsed.as_millis(),
|
||||
viewport_after.render_errors,
|
||||
viewport_after.last_render.as_millis(),
|
||||
);
|
||||
if soak_fps < 10.0 || soak_missed != 0 || viewport_after.render_errors != 0 {
|
||||
return Err("stable viewport did not sustain its 10 Hz target".into());
|
||||
}
|
||||
profile_warm_capture(&scene_source, limits, renderer_cancel.token()).await?;
|
||||
println!("LIVE_RENDER_OUTPUT={}", output.display());
|
||||
println!("LIVE_RENDER_SHA256={}", capture.image_sha256);
|
||||
@@ -281,10 +342,14 @@ async fn live_bevy_capture_renders_in_front_of_avatar() -> Result<(), Box<dyn Er
|
||||
.await;
|
||||
|
||||
scene_source.stop_prefetch().await;
|
||||
world_loop
|
||||
.shutdown()
|
||||
.map_err(|_| "world loop thread panicked")?;
|
||||
let _ = renderer_session.logout(renderer_cancel.token()).await;
|
||||
result
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)] // One ordered live timing record keeps phase boundaries explicit.
|
||||
async fn profile_warm_capture(
|
||||
source: &LibremetaverseSceneSource,
|
||||
limits: VisionLimits,
|
||||
@@ -311,9 +376,10 @@ async fn profile_warm_capture(
|
||||
.into_iter()
|
||||
.map(|(_, renderable)| renderable)
|
||||
.collect::<Vec<_>>();
|
||||
let renderables: Arc<[metacrate_rendering_wgpu::Renderable]> = renderables.into();
|
||||
let preparation = preparation_started.elapsed();
|
||||
let renderer_started = Instant::now();
|
||||
let renderer = metacrate_rendering_wgpu::Renderer::new_blocking()?;
|
||||
let renderer = metacrate_game_loop::ViewportRuntime::new_blocking()?;
|
||||
let renderer_initialization = renderer_started.elapsed();
|
||||
let render_limits = metacrate_rendering_wgpu::RenderLimits {
|
||||
width: limits.width,
|
||||
@@ -323,24 +389,38 @@ async fn profile_warm_capture(
|
||||
max_texture_pixels: limits.max_decode_pixels,
|
||||
far_distance: 512,
|
||||
};
|
||||
let first = renderer.render_profiled(
|
||||
let first = renderer.render_scene(
|
||||
scene.scene_sequence,
|
||||
scene.camera,
|
||||
&renderables,
|
||||
&scene.textures,
|
||||
|| Arc::clone(&renderables),
|
||||
Arc::clone(&scene.textures),
|
||||
[125, 149, 173, 255],
|
||||
render_limits,
|
||||
)?;
|
||||
let warm_started = Instant::now();
|
||||
let warm = renderer.render_profiled(
|
||||
let refresh_started = Instant::now();
|
||||
let refresh = renderer.render_scene(
|
||||
scene.scene_sequence.saturating_add(1),
|
||||
scene.camera,
|
||||
&renderables,
|
||||
&scene.textures,
|
||||
|| Arc::clone(&renderables),
|
||||
Arc::clone(&scene.textures),
|
||||
[125, 149, 173, 255],
|
||||
render_limits,
|
||||
)?;
|
||||
let refresh_total = refresh_started.elapsed();
|
||||
let warm_started = Instant::now();
|
||||
let warm = renderer.render_scene(
|
||||
scene.scene_sequence.saturating_add(1),
|
||||
scene.camera,
|
||||
|| panic!("unchanged scene was transferred to the renderer again"),
|
||||
Arc::clone(&scene.textures),
|
||||
[125, 149, 173, 255],
|
||||
render_limits,
|
||||
)?;
|
||||
let warm_render_total = warm_started.elapsed();
|
||||
let jpeg_started = Instant::now();
|
||||
let rgb = warm
|
||||
.frame
|
||||
.value
|
||||
.rgba
|
||||
.chunks_exact(4)
|
||||
.flat_map(|pixel| pixel[..3].iter().copied())
|
||||
@@ -354,7 +434,7 @@ async fn profile_warm_capture(
|
||||
)?;
|
||||
let jpeg_encode = jpeg_started.elapsed();
|
||||
println!(
|
||||
"LIVE_RENDER_PROFILE=scene_total_ms:{},terrain_wait_ms:{},object_snapshot_and_asset_discovery_ms:{},material_fetch_ms:{},asset_fetch_ms:{},decode_and_geometry_ms:{},renderable_collection_and_sort_ms:{},renderer_initialization_ms:{},first_render_total_ms:{},first_render:{:?},warm_render_total_ms:{},warm_render:{:?},jpeg_encode_ms:{},profile_total_ms:{}",
|
||||
"LIVE_RENDER_PROFILE=scene_total_ms:{},terrain_wait_ms:{},object_snapshot_and_asset_discovery_ms:{},material_fetch_ms:{},asset_fetch_ms:{},decode_and_geometry_ms:{},renderable_collection_and_sort_ms:{},renderer_initialization_ms:{},first_render_total_ms:{},first_render:{:?},scene_refresh_total_ms:{},scene_refresh:{:?},warm_render_total_ms:{},warm_render:{:?},jpeg_encode_ms:{},profile_total_ms:{}",
|
||||
scene_total.as_millis(),
|
||||
scene.timings.terrain_wait.as_millis(),
|
||||
scene
|
||||
@@ -368,11 +448,16 @@ async fn profile_warm_capture(
|
||||
renderer_initialization.as_millis(),
|
||||
render_total(&first.timings).as_millis(),
|
||||
first.timings,
|
||||
refresh_total.as_millis(),
|
||||
refresh.timings,
|
||||
warm_render_total.as_millis(),
|
||||
warm.timings,
|
||||
jpeg_encode.as_millis(),
|
||||
total_started.elapsed().as_millis(),
|
||||
);
|
||||
if refresh_total >= metacrate_game_loop::DEFAULT_VIEWPORT_FRAME_INTERVAL {
|
||||
return Err("cached scene refresh exceeded the viewport frame interval".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -400,8 +485,7 @@ impl SceneSource for DiagnosticSceneSource {
|
||||
Box::pin(async move {
|
||||
let mut scene = self.inner.capture_scene(generation, cancellation).await?;
|
||||
if let Some(material_override) = &self.material_override {
|
||||
for renderable in scene
|
||||
.entities
|
||||
for renderable in Arc::make_mut(&mut scene.entities)
|
||||
.iter_mut()
|
||||
.filter(|entity| entity.kind == SceneEntityKind::Object)
|
||||
.flat_map(|entity| &mut entity.renderables)
|
||||
|
||||
Reference in New Issue
Block a user