MONKEY (r18 A1 B004 streaming, world crate): chunk-cache key, nearest-first admission, stale-ready latch, emergency ground hold + collider timeout

World-crate part of benilla-everwood e9247623 (terrain_stream, spawn, wmo_portal).

(cherry picked from commit e9247623, benilla-app hunks omitted)
This commit is contained in:
pkuzic
2026-10-09 12:39:41 +01:00
parent ef6b5e673e
commit cc1dde91e2
3 changed files with 239 additions and 19 deletions
+186 -6
View File
@@ -75,6 +75,8 @@ pub struct TerrainStreamer {
map_dir: Option<String>,
/// The focus tile last streamed around, the furnisher's nearest-first key.
focus: (i32, i32),
/// The preload window moves at chunk boundaries, including inside the same tile.
focus_chunk: Option<(i32, i32)>,
/// The window's `(inner, outer)` half-widths last frame, logged once per change.
reach: Option<(i32, i32)>,
/// The map's WDT tile index: ADT requests wait for it and consult its `MAIN` grid.
@@ -98,6 +100,9 @@ pub struct TerrainStreamer {
const GLOBAL_WMO_UID: u32 = u32::MAX;
impl TerrainStreamer {
fn monkey_same_window(&self, window: StreamWindow) -> bool {
self.focus_chunk == Some(window.focus) && self.reach == Some((window.inner, window.outer))
}
/// `(spawned, requested)` tiles for the debug panel: furnished, and all in the window.
pub fn residency(&self) -> (usize, usize) {
let spawned = self.tiles.values().filter(|t| t.furnished).count();
@@ -405,6 +410,7 @@ impl Plugin for TerrainPlugin {
Update,
(
finish_colliders,
monkey_repair_wmo_residency,
stream_terrain,
furnish_tile_cells,
spawn_loaded_placements,
@@ -446,6 +452,13 @@ impl Plugin for TerrainPlugin {
.add_systems(Update, scope_placement_art)
// Ungated, so the density tracker never misses a boot-time config apply.
.add_systems(Update, rescatter_clutter)
.add_systems(
PostUpdate,
monkey_watch_wmo_residency
.after(crate::static_gx::MonkeyStaticGxCullSet)
.after(bevy::transform::TransformSystems::Propagate)
.run_if(crate::schedule::world_is_live),
)
.add_systems(
Update,
// After the interior claim: the reference resolves leaf, indoor and names from one
@@ -586,7 +599,7 @@ fn stream_terrain(
let center = focus.resolve(camera.single().ok().map(|c| c.translation));
let window = StreamWindow::at(view.farclip, center[0], center[1]);
let (cx, cy) = window.focus_tile();
let same_window = state.focus == (cx, cy) && state.reach == Some((window.inner, window.outer));
let same_window = state.monkey_same_window(window);
// Skip an unchanged frame only in a paced, complete, presentable world, since the tail it
// bypasses releases a load; even then the undrawn count stays fresh, so a rebake un-skips it.
if same_window
@@ -604,6 +617,7 @@ fn stream_terrain(
return;
}
state.focus = (cx, cy);
state.focus_chunk = Some(window.focus);
if state.reach != Some((window.inner, window.outer)) {
state.reach = Some((window.inner, window.outer));
info!(
@@ -726,10 +740,15 @@ fn stream_terrain(
// Spawn loaded tiles within the frame's budget. The material binds the shared light buffer,
// so a new tile is lit and fogged on its first frame.
let tile_deadline = Instant::now() + SPAWN_BUDGET;
for (&(tx, ty), tile) in state.tiles.iter_mut() {
if tile.entity.is_some() {
continue;
}
let mut landed: Vec<_> = state
.tiles
.iter()
.filter(|(_, t)| t.entity.is_none() && tiles.get(&t.handle).is_some())
.map(|(&key, _)| key)
.collect();
landed.sort_unstable_by_key(|&(tx, ty)| ((tx - cx).abs().max((ty - cy).abs()), tx, ty));
for (tx, ty) in landed {
let tile = state.tiles.get_mut(&(tx, ty)).unwrap();
let Some(adt) = tiles.get(&tile.handle) else {
continue; // not loaded yet, or missing
};
@@ -852,7 +871,7 @@ fn stream_terrain(
}
// The counting below latches off once fully ready (`complete`); a focus on unfurnished
// ground is a new load and re-arms it.
if !p.focus_resident {
if !p.focus_resident || !same_window {
p.complete = false;
}
// So does a snap onto furnished ground, whose buildings may still be arriving; on the edge
@@ -932,6 +951,92 @@ fn tile_drop_disabled() -> bool {
*ON.get_or_init(|| std::env::var_os("WOW_NO_TILE_DROP").is_some())
}
/// A resident building whose instance was lost must re-enter the ordinary spawn path. Inspect
/// once per 64 stream ticks; a failed asset has no instance and is deliberately not retried.
fn monkey_repair_wmo_residency(
mut commands: Commands,
mut state: ResMut<TerrainStreamer>,
placements: ResMut<Placements>,
instances: Query<(), With<crate::wmo_portal::WmoPortalInstance>>,
mut progress: ResMut<WorldLoadProgress>,
mut tick: Local<u32>,
) {
let scan = tick.is_multiple_of(64);
*tick = tick.wrapping_add(1);
if !scan {
return;
}
let placements = placements.into_inner();
for (&key, p) in &mut placements.by_id {
let Some(instance) = p.portal_instance else {
continue;
};
if !p.spawned || instances.contains(instance) {
continue;
}
warn!("MONKEY_WMO_RESIDENCY: uid={} instance={instance} lost with refs={}, re-registering building at {:?}",
key as u32, p.refs, bevy_to_wow(p.transform.translation));
placements.pending_spawns -= p.doodads.iter().filter(|d| !d.spawned).count();
for e in p.entities.drain(..) {
commands.entity(e).try_despawn();
}
p.doodads.clear();
p.portal_instance = None;
p.spawned = false;
placements.pending_spawns += 1;
state.settled = false;
progress.complete = false;
progress.scene_ready = false;
}
}
/// Record the three disappearance stages for the two reported buildings, only on a state edge.
#[allow(clippy::type_complexity)]
fn monkey_watch_wmo_residency(
placements: Res<Placements>,
instances: Query<&crate::wmo_portal::WmoPortalInstance>,
gx: Option<Res<crate::static_gx::StaticGx>>,
focus: Res<ViewFocus>,
camera: Query<&GlobalTransform, With<WorldCamera>>,
progress: Res<WorldLoadProgress>,
mut last: Local<HashMap<u32, (bool, bool, usize, usize, usize)>>,
) {
for uid in [71414, 54893] {
let placement = placements.by_id.get(&monkey_wmo_placement_key(uid));
let instance = placement.and_then(|p| p.portal_instance);
let portal = instance.and_then(|e| instances.get(e).ok());
let pvs = portal.map_or(0, |i| i.visible.iter().filter(|&&v| v).count());
let region = gx
.as_deref()
.and_then(|g| instance.and_then(|e| g.world.wmos.get(&e)));
let retained = region.map_or(0, |r| r.groups.len());
let admitted = gx
.as_deref()
.and_then(|g| {
instance.and_then(|e| {
g.world
.visible_wmos
.iter()
.find(|(i, _)| *i == e)
.map(|(_, s)| s)
})
})
.map_or(0, |s| s.drawn.iter().filter(|&&v| v).count());
let facts = (
placement.is_some(),
portal.is_some(),
pvs,
retained,
admitted,
);
if last.insert(uid, facts) != Some(facts) {
info!("MONKEY_WMO_WATCH: uid={uid} registered={} instance={instance:?} portal={} pvs={pvs} retained={retained} admitted={admitted} body={:?} eye={:?} focus={:?} resident={} presentable={}",
facts.0, facts.1, focus.body_pos(), camera.iter().next().map(|c| bevy_to_wow(c.translation())),
progress.focus_tile, progress.focus_resident, progress.presentable());
}
}
}
/// Register one M2 doodad placement, or bump its refcount if already known.
fn register_doodad(
placements: &mut Placements,
@@ -1009,6 +1114,7 @@ fn drop_streamed_world(
staticgx: Option<&mut crate::static_gx::StaticGx>,
activity: &mut StreamActivity,
) {
state.focus_chunk = None;
for ((_tx, _ty), t) in state.tiles.drain() {
despawn_tile_owned(commands, &t);
activity.tiles_dropped += 1;
@@ -1307,6 +1413,35 @@ mod focus_tests {
Some(wow_to_bevy([SPAWN_XY.0, SPAWN_XY.1, 100.0]))
}
#[test]
fn monkey_a_chunk_crossing_inside_one_tile_reopens_the_preload_window() {
// Default farclip reaches 13 chunks. Advancing from chunk 2 to 3 reaches tile +1.
let before = StreamWindow {
focus: (32 * 16 + 2, 44 * 16 + 2),
inner: 11,
outer: 13,
};
let after = StreamWindow {
focus: (32 * 16 + 3, 44 * 16 + 2),
..before
};
assert_eq!(before.focus_tile(), after.focus_tile());
assert!(!before.wanted_tiles().contains(&(33, 44)));
assert!(after.wanted_tiles().contains(&(33, 44)));
let state = TerrainStreamer {
focus: before.focus_tile(),
focus_chunk: Some(before.focus),
reach: Some((before.inner, before.outer)),
settled: true,
..Default::default()
};
assert!(state.monkey_same_window(before));
assert!(
!state.monkey_same_window(after),
"same tile must not suppress an incoming row"
);
}
#[test]
fn the_pick_outranks_the_camera_until_the_avatar_exists() {
let idle = ViewFocus::entry(PICK.0, PICK.1);
@@ -1382,6 +1517,51 @@ mod straddler_tests {
}
}
#[test]
fn monkey_a_resident_wmo_with_a_lost_instance_is_requeued_without_losing_its_refs() {
let mut app = App::new();
app.init_resource::<TerrainStreamer>()
.init_resource::<Placements>()
.init_resource::<WorldLoadProgress>();
let missing = app.world_mut().spawn_empty().id();
app.world_mut().despawn(missing);
let orphan = app.world_mut().spawn_empty().id();
let mut p = straddler((0, 0), vec![orphan]);
p.model = ModelHandle::Wmo(Default::default());
p.portal_instance = Some(missing);
p.refs = 3;
let key = monkey_wmo_placement_key(71414);
app.world_mut()
.resource_mut::<Placements>()
.by_id
.insert(key, p);
app.world_mut().resource_mut::<TerrainStreamer>().settled = true;
app.world_mut().resource_mut::<WorldLoadProgress>().complete = true;
app.world_mut()
.run_system_once(monkey_repair_wmo_residency)
.unwrap();
let placements = app.world().resource::<Placements>();
let p = &placements.by_id[&key];
assert_eq!(p.refs, 3);
assert!(!p.spawned && p.portal_instance.is_none());
assert_eq!(placements.pending_spawns, 1);
assert!(!app.world().resource::<TerrainStreamer>().settled);
assert!(!app.world().resource::<WorldLoadProgress>().complete);
assert!(app.world().get_entity(orphan).is_err());
// A retired missing asset has no recorded instance: don't retry it forever.
app.world_mut()
.resource_mut::<Placements>()
.by_id
.get_mut(&key)
.unwrap()
.spawned = true;
app.world_mut().resource_mut::<Placements>().pending_spawns = 0;
app.world_mut()
.run_system_once(monkey_repair_wmo_residency)
.unwrap();
assert_eq!(app.world().resource::<Placements>().pending_spawns, 0);
}
fn run_handoff(world: &mut World, dead: (i32, i32), uids: Vec<u32>, merge_on: bool) {
world
.run_system_once(
@@ -161,7 +161,15 @@ pub(super) fn spawn_loaded_placements(
// follows the work, so every frame spawns at least one.
let deadline = Instant::now() + SPAWN_BUDGET;
'placements: for (&key, p) in by_id.iter_mut() {
// Load the buildings nearest the body before scenery at the far edge of the window.
let mut due: Vec<_> = by_id
.iter()
.filter(|(_, p)| !p.spawned || p.doodads.iter().any(|d| !d.spawned))
.map(|(&key, p)| (placement_priority(&streamer, p.transform.translation), key))
.collect();
due.sort_unstable();
'placements: for (_, key) in due {
let p = by_id.get_mut(&key).unwrap();
let unique_id = key as u32;
// 1. The model's own geometry, once; a WMO also resolves its doodad props for step 2.
if !p.spawned {
@@ -174,7 +182,10 @@ pub(super) fn spawn_loaded_placements(
// `alliancebrasscannon_flat.m2`, 2026-09-08). Retire it as an empty
// spawn so the accounting completes; the reference draws nothing there
// either.
if matches!(asset_server.load_state(h), bevy::asset::LoadState::Failed(_)) {
if matches!(
asset_server.load_state(h),
bevy::asset::LoadState::Failed(_)
) {
warn!("placement {unique_id}: its model failed to load — spawning nothing");
p.spawned = true;
*pending_spawns -= 1;
@@ -345,8 +356,13 @@ pub(super) fn spawn_loaded_placements(
let Some(m) = wmos.get(h) else {
// MONKEY (missing model): see the M2 arm — a WMO the client lacks
// retires as an empty spawn rather than pinning the loading screen.
if matches!(asset_server.load_state(h), bevy::asset::LoadState::Failed(_)) {
warn!("placement {unique_id}: its WMO failed to load — spawning nothing");
if matches!(
asset_server.load_state(h),
bevy::asset::LoadState::Failed(_)
) {
warn!(
"placement {unique_id}: its WMO failed to load — spawning nothing"
);
p.spawned = true;
*pending_spawns -= 1;
activity.placements_spawned += 1;
@@ -380,13 +396,19 @@ pub(super) fn spawn_loaded_placements(
refs: &m.portal_refs,
slices: &sky_slices,
},
m.submeshes.iter().zip(m.submesh_group.iter()).map(|(s, g)| {
(
*g,
matches!(s.wmo_batch, Some(benilla_formats::WmoBatchClass::Ext)),
&s.geometry.positions[..],
)
}),
m.submeshes
.iter()
.zip(m.submesh_group.iter())
.map(|(s, g)| {
(
*g,
matches!(
s.wmo_batch,
Some(benilla_formats::WmoBatchClass::Ext)
),
&s.geometry.positions[..],
)
}),
);
let instance = (has_portals || m.wmo_id != 0).then(|| {
commands
@@ -681,7 +703,8 @@ pub(super) fn spawn_loaded_placements(
.unwrap_or_default();
let slices = wmo.map(fx::portal_slices).unwrap_or_default();
// MONKEY (review fixes): the claim list owns its placement even when no MODR names it.
let claims = wmo.zip(portal_instance)
let claims = wmo
.zip(portal_instance)
.map(|(w, instance)| fx::PropClaims::new(w, &slices, placement, instance));
for d in &mut p.doodads {
if d.spawned {
@@ -691,7 +714,10 @@ pub(super) fn spawn_loaded_placements(
// MONKEY (missing model): a prop whose M2 the client doesn't ship would hold its
// BUILDING un-"up" (`placements_pending`) for the loading screen's whole life —
// the 2371 s Stormwind hang on `alliancebrasscannon_flat.m2`. Retire the prop.
if matches!(asset_server.load_state(&d.handle), bevy::asset::LoadState::Failed(_)) {
if matches!(
asset_server.load_state(&d.handle),
bevy::asset::LoadState::Failed(_)
) {
warn!("placement {unique_id}: a WMO prop's model failed to load — skipping it");
d.spawned = true;
*pending_spawns -= 1;
@@ -298,7 +298,11 @@ fn compute_wmo_pvs(
mut camera_fog: ResMut<CameraWmoFog>,
mut camera_claim: ResMut<CameraInteriorClaim>,
mut camera_windows: ResMut<ExteriorWindows>,
mut monkey_last_stall: Local<Option<std::time::Instant>>,
) {
let monkey_started = std::time::Instant::now();
let mut monkey_instances = 0u32;
let mut monkey_walks = 0u64;
// No world camera yet: keep last frame's sets, all visible.
let Some((cam_t, proj)) = cam.iter().next() else {
return;
@@ -337,6 +341,7 @@ fn compute_wmo_pvs(
continue;
};
let groups = model.group_nav.len();
monkey_instances += 1;
// A WMO with no portal graph (single-group props, doors) is never culled, and never on the
// interior-fog lane. Written through the change gate: marking every prop changed each frame
// would defeat `Changed<WmoPortalInstance>`.
@@ -376,6 +381,7 @@ fn compute_wmo_pvs(
&world_from_local,
&mut (&mut tap, &mut log),
);
monkey_walks += u64::from(fresh.iters);
if inst.bypass_change_detection().visible != fresh.visible {
inst.bypass_change_detection().visible = fresh.visible;
inst.set_changed();
@@ -472,6 +478,14 @@ fn compute_wmo_pvs(
if *camera_windows != want_windows {
*camera_windows = want_windows;
}
// A moving dungeon probe needs timed CPU evidence; snapshots cannot identify a portal hitch.
let monkey_ms = monkey_started.elapsed().as_secs_f32() * 1000.0;
if monkey_ms >= 8.0 && monkey_last_stall.is_none_or(|last| last.elapsed().as_secs_f32() >= 1.0)
{
*monkey_last_stall = Some(std::time::Instant::now());
warn!("MONKEY_WMO_STALL: cpu_ms={monkey_ms:.2} instances={monkey_instances} walk_steps={monkey_walks} eye={:?} claim={:?}",
bevy_to_wow(eye_world), camera_claim.0.as_ref().map(|c| c.room));
}
if let Some((mut text, path)) = dump_text.zip(dump_to) {
// The frame's published verdict, after every placement: the claim and the windows.
text.push_str(&format!("CAMERA CLAIM: {:?}\n", camera_claim.0));