Skip to main content

concinnity_engine/gfx/streaming_system/
mod.rs

1// src/gfx/streaming_system/mod.rs
2//
3// StreamingSystem: drives the asset-streaming pools (albedo/normal texture,
4// mesh geometry, and infinite voxel-world chunks), and publishes the
5// camera-relative view the draw consumes. Streaming policy (scoring, dispatch,
6// residency) runs here; the GPU effects are recorded into the frame's op
7// queue with owned payloads and replayed at submission, with slot decisions
8// from the engine's `RenderSlots` allocator. An upload the backend refuses
9// comes back one tick later as a `RenderOpFailures` entry and is rolled back
10// at the top of the next step.
11//
12// Scheduled immediately before GraphicsSystem, so a chunk world's view rebase
13// (see `CameraRelativeView`) is ready for this same frame's submit, and any
14// recorded texture / mesh upload lands before the draw. GraphicsSystem's init
15// builds the streamers (world content + backend support) and parks them here
16// as the `StreamingState` resource; each step takes it and puts it back, so
17// the state and the `PipelineContext` are never borrowed together (the same
18// handoff the settings and overlay states use).
19//
20// The streamers themselves (the OS-coupled worker threads + channels) live in
21// `crate::gfx::streaming::{texture, mesh, chunk}`; this module only
22// scores, dispatches, and applies their results each frame.
23
24use crate::components::Camera3D;
25use crate::ecs::asset_id::AssetId;
26use crate::ecs::{PipelineContext, RenderOpFailures, StepResult, System};
27use crate::gfx::backend::ChunkMesh;
28use crate::gfx::ops::{OpFailure, RenderOps};
29use crate::gfx::overlay::OverlayFrame;
30use crate::gfx::render_slots::RenderSlots;
31use crate::gfx::scene_residency::{CHANNEL_MESH, CHANNEL_SHADER, CHANNEL_TEXTURE, SceneResidency};
32
33pub(crate) mod accounting;
34pub(crate) mod pressure;
35pub(crate) mod stats_log;
36
37const IDENTITY4: [[f32; 4]; 4] = crate::gfx::draw_list::IDENTITY4;
38
39// Throttled RSS sampling cadence for the process-RAM back-off valve. RSS is a
40// syscall, so the valve re-evaluates ~2x/second (every 30 frames near 60 fps)
41// off the frame clock rather than every frame.
42const PRESSURE_SAMPLE_INTERVAL: u64 = 30;
43
44// The camera-relative view + position GraphicsSystem hands to `update_view` /
45// `draw_frame`. Published every frame by StreamingSystem: the world's absolute
46// view + camera position when no `VoxelWorld` is streaming, or both rebased
47// onto the chunk render origin when one is (so an unbounded world renders from
48// small coordinates without large-coordinate jitter). GraphicsSystem falls back
49// to the absolute `Camera3D` values if this resource is absent (a unit test
50// driving GraphicsSystem without StreamingSystem).
51#[derive(Debug, Clone, Copy)]
52pub(crate) struct CameraRelativeView {
53    pub view: [[f32; 4]; 4],
54    pub cam_pos: [f32; 3],
55}
56
57// Runtime state for streaming an infinite `VoxelWorld`: the chunk streamer,
58// the resident chunk-to-draw-index map, and the per-chunk render parameters
59// (chunk size for the camera-to-chunk mapping and model placement, plus the
60// shared material every chunk draws with).
61pub(crate) struct ChunkStreamState {
62    pub(crate) streamer: crate::gfx::streaming::chunk::ChunkStreamer,
63    // Maps a resident chunk's coordinate to its `DrawObject` index.
64    pub(crate) draws: std::collections::BTreeMap<crate::gfx::chunk_coord::ChunkCoord, usize>,
65    pub(crate) chunk_w: f32,
66    pub(crate) chunk_d: f32,
67    // Render origin for camera-relative rendering: the chunk every resident
68    // chunk's model matrix is currently placed relative to. It follows the
69    // camera's chunk; when it changes the resident chunks are rebased onto the
70    // new origin.
71    pub(crate) origin_chunk: crate::gfx::chunk_coord::ChunkCoord,
72    pub(crate) texture_slot: usize,
73    pub(crate) normal_map_slot: usize,
74    pub(crate) material: crate::gfx::render_types::MaterialUniforms,
75}
76
77/// `(resident, pending, unloaded)` counts for each streaming pool, or `None`
78/// when that pool is not streaming. Read by the debug server's `streaming`
79/// command for headless verification. Only the `cn debug` binary consumes it,
80/// so it reads as dead code in a plain library build.
81#[derive(Debug, Clone, Default)]
82pub struct StreamingStats {
83    /// `(resident, pending, budget)` texture counts when streaming.
84    pub texture: Option<(usize, usize, usize)>,
85    /// `(resident, pending, budget)` mesh counts when streaming.
86    pub mesh: Option<(usize, usize, usize)>,
87    /// `(resident, pending)` chunk counts when a `VoxelWorld` is streaming.
88    pub chunk: Option<(usize, usize)>,
89    /// `(resident_bytes, byte_budget)` for the texture pool when streaming;
90    /// `byte_budget` is 0 when the pool runs count-only (no byte budget).
91    pub texture_bytes: Option<(u64, u64)>,
92    /// `(resident_bytes, byte_budget)` for the mesh pool when streaming.
93    pub mesh_bytes: Option<(u64, u64)>,
94    /// `(resident_bytes, byte_budget)` for the chunk pool when a VoxelWorld is
95    /// streaming; `byte_budget` is 0 when the GPU reported no memory figure.
96    pub chunk_bytes: Option<(u64, u64)>,
97}
98
99/// Live process-RAM pressure on streaming, published by StreamingSystem on each
100/// throttled sample when a `MemoryBudget` is present. `under_pressure` is true
101/// whenever the back-off valve is engaged (gating loads or evicting). Read by the
102/// debug server's `streaming` command for headless verification; harmless (and
103/// unread) in a plain `cn run`. Absent entirely when no `MemoryBudget` is
104/// published or RSS cannot be queried, in which case the valve is inert.
105#[derive(Debug, Clone, Copy)]
106pub struct StreamingPressure {
107    /// Process resident-set size at the sample.
108    pub rss_bytes: u64,
109    /// The published memory budget.
110    pub budget_bytes: u64,
111    /// Whether the back-off valve is engaged.
112    pub under_pressure: bool,
113}
114
115// The streaming pools GraphicsSystem's init builds and hands off. Held as a
116// parked resource; StreamingSystem takes it each step, drives the pools, and
117// puts it back. `frame_count` is this system's own frame clock, incremented
118// once per step; it stays in lockstep with GraphicsSystem's (both start at 0
119// and tick once per world step), so eviction retire-frames and the LRU scores
120// use the same frame number the draw does.
121pub(crate) struct StreamingState {
122    // Shared albedo + normal-map texture pool streamer. `Some` only when a
123    // `StreamingConfig` was declared.
124    pub(crate) texture_streamer: Option<crate::gfx::streaming::texture::TextureStreamer>,
125    // Mesh-geometry streamer. `Some` under the same condition as above.
126    pub(crate) mesh_streamer: Option<crate::gfx::streaming::mesh::MeshStreamer>,
127    // Maps a streamed mesh's id to its DrawObject index, so completed loads and
128    // evictions are applied to the right draw. Empty when not streaming.
129    pub(crate) mesh_stream_draw_indices: Vec<usize>,
130    // Infinite voxel-world chunk streaming. `Some` only when a `VoxelWorld` was
131    // declared.
132    pub(crate) chunk_stream: Option<ChunkStreamState>,
133    // Deferred shader-bucket pipelines, warmed one per frame as their scene
134    // pins. `Some` only when init deferred at least one bucket.
135    pub(crate) shader_warmup: Option<crate::gfx::streaming::shader::ShaderWarmup>,
136    // Scene-pinned residency over the texture/mesh pools. `Some` only when the
137    // world declares scenes and at least one pool streams; unpinned scenes'
138    // members are blocked on the planners (never load, evict if resident).
139    pub(crate) scene_residency: Option<SceneResidency>,
140    // This system's frame clock (see the struct doc).
141    pub(crate) frame_count: u64,
142    // Frames the backend keeps in flight: an eviction's freed region cannot be
143    // reused until the command buffers that drew it retire, at
144    // `frame_count + frames_in_flight`.
145    pub(crate) frames_in_flight: usize,
146    // Baseline (derived at setup) resident-byte budget for each pool; `None`
147    // when the pool runs count-only (chunks: when no VoxelWorld streams or the
148    // GPU reports no memory). The RAM back-off valve reduces the live budget
149    // below this under deep pressure and restores it exactly on release.
150    pub(crate) texture_baseline_budget: Option<u64>,
151    pub(crate) mesh_baseline_budget: Option<u64>,
152    pub(crate) chunk_baseline_budget: Option<u64>,
153    // Process-RAM back-off valve state (see `pressure`), re-evaluated on the
154    // throttled RSS sample. `pressure_factor` is the byte-budget scale currently
155    // applied to the pools (1.0 = baseline); `last_sampled_rss` feeds the
156    // "still rising" escalation from stage 1 to stage 2.
157    pub(crate) pressure_stage: pressure::StreamPressureStage,
158    pub(crate) pressure_factor: f64,
159    pub(crate) last_sampled_rss: Option<u64>,
160    // Long-session memory drift, folded from the same throttled sample. Purely
161    // reported: it names what grew, and never moves the valve.
162    pub(crate) drift: crate::app::mem_drift::DriftTracker,
163    // Last verdict logged, so a steady session states its reading once rather
164    // than twice a second.
165    pub(crate) last_drift_verdict: Option<crate::app::mem_drift::DriftVerdict>,
166    // Gates the periodic per-pool counter line so a settled pool logs its
167    // counts once rather than every sample.
168    pub(crate) heartbeats: stats_log::PoolHeartbeats,
169}
170
171impl std::fmt::Debug for StreamingState {
172    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173        f.debug_struct("StreamingState")
174            .field("frame_count", &self.frame_count)
175            .field("texture", &self.texture_streamer.is_some())
176            .field("mesh", &self.mesh_streamer.is_some())
177            .field("chunk", &self.chunk_stream.is_some())
178            .field("pressure", &self.pressure_stage)
179            .finish()
180    }
181}
182
183#[derive(Debug, Default)]
184/// Drives texture / mesh / chunk residency against the streaming budgets.
185pub struct StreamingSystem {
186    // Scene-status scratch reused across frames, compared against the
187    // published `SceneResidencyStatus` before republishing.
188    scene_status_scratch: Vec<(AssetId, crate::gfx::scene_residency::SceneLoadState, f32)>,
189}
190
191impl StreamingSystem {
192    /// A system with empty scratch.
193    pub fn new() -> Self {
194        Self::default()
195    }
196}
197
198impl System for StreamingSystem {
199    fn step(&mut self, ctx: &mut PipelineContext) -> StepResult {
200        // No parked state (graphics init has not succeeded): nothing to drive,
201        // and GraphicsSystem is not drawing either, so no view to publish.
202        if !ctx.resources.contains::<StreamingState>() {
203            return StepResult::Continue;
204        }
205
206        // Everything the drive needs from the wider context, gathered first so
207        // the state below is borrowed in place instead of moved out of its
208        // resource slot (which would free and re-box it every frame).
209        let ram_budget = ctx
210            .resource::<crate::app::budget::MemoryBudget>()
211            .map(|b| b.budget_bytes);
212        // The camera the draw will use (written by the camera controller last
213        // tick) is the absolute fallback when no chunk streaming rebases it.
214        let (view_matrix, cam_pos) = ctx
215            .query::<Camera3D>()
216            .next()
217            .map(|c| (c.view_matrix, c.position))
218            .unwrap_or((IDENTITY4, [0.0; 3]));
219        // Peek (not take) the overlay's world-hidden flag: OverlaySystem
220        // published it first this tick and GraphicsSystem takes it later.
221        // Streaming pauses behind an opaque menu (the world is not drawn),
222        // unless a pinned scene is still loading: a loading screen's opaque
223        // backdrop must not starve the load it reports (see `drive`).
224        let world_hidden = ctx
225            .resource::<OverlayFrame>()
226            .map(|o| o.world_hidden)
227            .unwrap_or(false);
228
229        // Scene pins for streamed-content residency: the active scene, plus a
230        // fade target mid-transition so the destination starts loading before
231        // visibility flips.
232        let pin_pair: Option<([AssetId; 2], usize)> = ctx
233            .resource::<crate::ecs::ActiveSceneFlow>()
234            .and_then(|slot| slot.flow.as_ref())
235            .map(|flow| {
236                let mut pins = [flow.current; 2];
237                let mut len = 1;
238                if let crate::gfx::scene_flow::FadePhase::ToBlack { next, .. } = flow.fade
239                    && next != flow.current
240                {
241                    pins[1] = next;
242                    len = 2;
243                }
244                (pins, len)
245            });
246        let scene_pins: Option<&[AssetId]> = pin_pair.as_ref().map(|(pins, len)| &pins[..*len]);
247        let transient_pool_bytes = ctx.profile.render.transient_pool_bytes;
248
249        // Throttled process-RAM back-off sample (~2x/sec). Reads the world's
250        // `MemoryBudget` ceiling and live RSS; when RSS nears the ceiling the
251        // valve engages (stage 1 gates new loads, stage 2 shrinks residency).
252        // Skipped entirely when no `MemoryBudget` is published or RSS is
253        // unavailable, leaving streaming on its byte-budget policy unchanged.
254        {
255            let mut pressure_sample = None;
256            let mut drift_sample = None;
257            let state = ctx
258                .resources
259                .get_mut::<StreamingState>()
260                .expect("presence checked above");
261            if state.frame_count.is_multiple_of(PRESSURE_SAMPLE_INTERVAL)
262                && let Some(budget) = ram_budget
263            {
264                let rss = crate::app::sysmem::process_resident_bytes();
265                pressure_sample = state.sample_pressure(rss, budget);
266                drift_sample = state.sample_drift(rss, budget);
267            }
268            if let Some(pressure) = pressure_sample {
269                ctx.insert_resource(pressure);
270            }
271            if let Some(drift) = drift_sample {
272                ctx.insert_resource(drift);
273            }
274        }
275
276        // The recording surfaces graphics init published beside this state.
277        // Taken out for the drive so `ctx` stays freely borrowable.
278        let Some(mut queues) = crate::ecs::ActiveRenderQueues::take(ctx.resources) else {
279            // Should not happen (published together with this state): publish
280            // the absolute view so the draw is still driven.
281            ctx.insert_resource(CameraRelativeView {
282                view: view_matrix,
283                cam_pos,
284            });
285            return StepResult::Continue;
286        };
287        let failures = ctx.resources.remove::<RenderOpFailures>();
288
289        let state = ctx
290            .resources
291            .get_mut::<StreamingState>()
292            .expect("presence checked above");
293
294        // Roll back the ops that failed at the previous frame's replay (a
295        // refused streamed-mesh upload, a failed chunk add) before planning,
296        // so this frame's dispatch sees the corrected residency.
297        if let Some(failures) = failures {
298            state.apply_op_failures(&failures.0, &mut queues.slots);
299        }
300
301        let (view, cam_pos) = state.drive(
302            &mut queues.ops,
303            &mut queues.slots,
304            cam_pos,
305            view_matrix,
306            world_hidden,
307            scene_pins,
308        );
309
310        // Refresh each pool's device footprint under the shared tags, so a
311        // readout can name what VRAM is holding.
312        accounting::publish(
313            concinnity_core::memory::ledger(),
314            state.pool_reports(transient_pool_bytes),
315        );
316        // Per-scene load status into the reused scratch; published below once
317        // the state borrow has ended.
318        let have_residency = match state.scene_residency.as_ref() {
319            Some(residency) => {
320                residency.status_into(&mut self.scene_status_scratch);
321                true
322            }
323            None => false,
324        };
325
326        crate::ecs::ActiveRenderQueues::put(ctx.resources, queues);
327        ctx.insert_resource(CameraRelativeView { view, cam_pos });
328        // Republish the per-scene load status when it changed, so menus and
329        // loading screens can read scene progress without touching the pools.
330        if have_residency {
331            match ctx.resource_mut::<crate::ecs::SceneResidencyStatus>() {
332                Some(published) => {
333                    if published.scenes != self.scene_status_scratch {
334                        published.scenes.clone_from(&self.scene_status_scratch);
335                    }
336                }
337                None => {
338                    ctx.insert_resource(crate::ecs::SceneResidencyStatus {
339                        scenes: self.scene_status_scratch.clone(),
340                    });
341                }
342            }
343        }
344        StepResult::Continue
345    }
346}
347
348impl StreamingState {
349    // Apply this frame's pending shader-bucket work: build the pipeline for
350    // one bucket whose scene just pinned, or release one whose scene unpinned.
351    //
352    // A bucket that cannot be installed (unreadable payload, a shader missing
353    // the bindless entry points) is recorded resident anyway after the error:
354    // its draws stay skipped, but the owning scene finishes loading instead of
355    // holding its loading screen open forever on work that will never succeed.
356    fn drive_shader_warmup(&mut self, ops: &mut RenderOps) {
357        let Some((bucket, want_resident)) =
358            self.shader_warmup.as_ref().and_then(|w| w.next_pending())
359        else {
360            return;
361        };
362        let resident = if want_resident {
363            match self.shader_warmup.as_ref().map(|w| w.load(bucket)) {
364                Some(Ok(stages)) => {
365                    // The payload is in hand; the recorded install is what
366                    // ends the deferral. Pipeline creation is device work, so
367                    // it runs (and is timed) at replay beside the draw.
368                    ops.record(move |backend| {
369                        let shader = crate::gfx::backend_init::ShaderBytes {
370                            vert: &stages.vert,
371                            frag: &stages.frag,
372                            shadow: &[],
373                            vert_instanced: &stages.vert_instanced,
374                            deferred: false,
375                        };
376                        let started = std::time::Instant::now();
377                        match backend.install_world_shader(bucket, shader) {
378                            // The elapsed time is the frame cost this warmup
379                            // keeps out of gameplay.
380                            Ok(()) => tracing::info!(
381                                "StreamingSystem: shader bucket {} pipeline ready ({:.1} ms)",
382                                bucket,
383                                started.elapsed().as_secs_f32() * 1000.0
384                            ),
385                            Err(e) => tracing::error!(
386                                "StreamingSystem: shader bucket {} pipeline build failed: {}",
387                                bucket,
388                                e
389                            ),
390                        }
391                    });
392                }
393                Some(Err(e)) => tracing::error!(
394                    "StreamingSystem: shader bucket {} payload unreadable: {}",
395                    bucket,
396                    e
397                ),
398                None => {}
399            }
400            true
401        } else {
402            ops.record(move |backend| {
403                backend.evict_world_shader(bucket);
404                tracing::info!(
405                    "StreamingSystem: shader bucket {} pipeline released",
406                    bucket
407                );
408            });
409            false
410        };
411        if let Some(w) = self.shader_warmup.as_mut() {
412            w.note_resident(bucket, resident);
413        }
414        if let Some(residency) = self.scene_residency.as_mut() {
415            residency.note_resident((CHANNEL_SHADER, bucket), resident);
416        }
417    }
418
419    // Roll back the recorded ops that failed at the previous frame's replay:
420    // a refused streamed-mesh upload returns to `Unloaded` (retried once
421    // freed space reclaims), a failed chunk add drops its tracking and frees
422    // its draw slot.
423    pub(crate) fn apply_op_failures(&mut self, failures: &[OpFailure], slots: &mut RenderSlots) {
424        for &failure in failures {
425            match failure {
426                OpFailure::MeshUpload { stream_id } => {
427                    if let Some(streamer) = &mut self.mesh_streamer {
428                        streamer.note_upload_failed(stream_id);
429                    }
430                    if let Some(residency) = &mut self.scene_residency {
431                        residency.note_resident((CHANNEL_MESH, stream_id as u32), false);
432                    }
433                }
434                OpFailure::ChunkAdd { coord } => {
435                    if let Some(cs) = &mut self.chunk_stream
436                        && let Some(draw_idx) = cs.draws.remove(&coord)
437                    {
438                        slots.free_draw(draw_idx);
439                        tracing::warn!(
440                            "StreamingSystem: chunk add ({},{}) rolled back",
441                            coord.x,
442                            coord.z
443                        );
444                    }
445                }
446            }
447        }
448    }
449
450    // Score, dispatch, and apply this frame's streaming for every active pool,
451    // then return the camera-relative view + position the draw should use
452    // (absolute unless a `VoxelWorld` rebases them). Advances the frame clock.
453    fn drive(
454        &mut self,
455        ops: &mut RenderOps,
456        slots: &mut RenderSlots,
457        cam_pos: [f32; 3],
458        view_matrix: [[f32; 4]; 4],
459        world_hidden: bool,
460        scene_pins: Option<&[AssetId]>,
461    ) -> ([[f32; 4]; 4], [f32; 3]) {
462        // Stage 1 of the RAM back-off valve freezes new load dispatch: the pools
463        // keep their current residency but stop growing. Stage 2 keeps
464        // dispatching (under a reduced byte budget) so the planner can evict.
465        let loads_frozen = self.pressure_stage.freezes_loads();
466
467        // Sync scene pins onto the pools: members of a scene leaving the pin
468        // set are blocked (never load, evict next plan), members of a scene
469        // entering it unblock and stream in through the normal planning path.
470        if let (Some(residency), Some(pins)) = (self.scene_residency.as_mut(), scene_pins) {
471            let changes = residency.sync_pins(pins);
472            for (members, blocked) in [(&changes.blocked, true), (&changes.unblocked, false)] {
473                for &(channel, id) in members {
474                    match channel {
475                        CHANNEL_TEXTURE => {
476                            if let Some(s) = &mut self.texture_streamer {
477                                s.set_blocked(id as usize, blocked);
478                            }
479                        }
480                        CHANNEL_MESH => {
481                            if let Some(s) = &mut self.mesh_streamer {
482                                s.set_blocked(id as usize, blocked);
483                            }
484                        }
485                        CHANNEL_SHADER => {
486                            if let Some(w) = &mut self.shader_warmup {
487                                w.set_blocked(id, blocked);
488                            }
489                        }
490                        _ => {}
491                    }
492                }
493            }
494        }
495
496        // Warm (or release) one shader bucket's pipeline per frame, so a
497        // scene owning several shaders spreads the device work over the
498        // frames its loading screen is already up rather than stalling one of
499        // them.
500        self.drive_shader_warmup(ops);
501
502        // A pinned scene mid-load keeps the pools dispatching even while the
503        // world is hidden, so a loading screen's opaque backdrop does not
504        // pause the very load whose progress it shows.
505        let world_hidden = world_hidden
506            && !self
507                .scene_residency
508                .as_ref()
509                .is_some_and(|r| r.any_loading());
510
511        // Drive albedo-texture streaming: re-score every slot by camera
512        // distance, dispatch this frame's background loads within budget, then
513        // apply completed uploads + evictions. Each backend's
514        // update_texture_slot rewrites whichever descriptors / argument-buffers
515        // sample that slot so it takes effect on this same draw_frame.
516        if !world_hidden && let Some(streamer) = &mut self.texture_streamer {
517            streamer.update_scores(cam_pos, self.frame_count);
518            if !loads_frozen {
519                for slot in streamer.plan_and_dispatch() {
520                    ops.record(move |backend| {
521                        if let Err(e) = backend.evict_texture_slot(slot) {
522                            tracing::warn!("StreamingSystem: texture evict slot {}: {}", slot, e);
523                        }
524                    });
525                    if let Some(residency) = self.scene_residency.as_mut() {
526                        residency.note_resident((CHANNEL_TEXTURE, slot as u32), false);
527                    }
528                }
529            }
530            let residency = &mut self.scene_residency;
531            streamer.drain_completed(self.frame_count, |slot, image| {
532                ops.record(move |backend| {
533                    if let Err(e) = backend.update_texture_slot(slot, &image) {
534                        tracing::warn!("StreamingSystem: texture upload slot {}: {}", slot, e);
535                    }
536                });
537                if let Some(residency) = residency.as_mut() {
538                    residency.note_resident((CHANNEL_TEXTURE, slot as u32), true);
539                }
540            });
541            // Surface streaming progress as it moves so a headless run can
542            // confirm textures are coming resident.
543            if let Some((resident, pending, unloaded)) = self
544                .heartbeats
545                .texture
546                .sample(self.frame_count, || streamer.stats())
547            {
548                tracing::info!(
549                    "StreamingSystem: texture streaming -- {} resident, {} pending, {} unloaded",
550                    resident,
551                    pending,
552                    unloaded
553                );
554            }
555        }
556
557        // Drive mesh-geometry streaming: re-score each streamed mesh by camera
558        // distance, dispatch this frame's background loads, then apply completed
559        // geometry uploads + evictions. A mesh is skipped in every pass until
560        // its geometry region is resident.
561        if !world_hidden && let Some(streamer) = &mut self.mesh_streamer {
562            streamer.update_scores(cam_pos, self.frame_count);
563            if !loads_frozen {
564                // A runtime eviction's freed space must not be reused until the
565                // in-flight command buffers that drew it retire.
566                let retire_frame = self.frame_count + self.frames_in_flight as u64;
567                for stream_id in streamer.plan_and_dispatch() {
568                    if let Some(&draw_idx) = self.mesh_stream_draw_indices.get(stream_id) {
569                        ops.record(move |backend| {
570                            if let Err(e) = backend.evict_mesh(draw_idx, retire_frame) {
571                                tracing::warn!(
572                                    "StreamingSystem: mesh evict draw {}: {}",
573                                    draw_idx,
574                                    e
575                                );
576                            }
577                        });
578                    }
579                    if let Some(residency) = self.scene_residency.as_mut() {
580                        residency.note_resident((CHANNEL_MESH, stream_id as u32), false);
581                    }
582                }
583            }
584            let draw_indices = &self.mesh_stream_draw_indices;
585            let frame = self.frame_count;
586            let residency = &mut self.scene_residency;
587            streamer.drain_completed(self.frame_count, |stream_id, verts, idxs| {
588                // The mesh is marked resident on handoff; a transient
589                // seed-full refusal comes back as an op failure and
590                // `apply_op_failures` rolls it back to Unloaded next tick.
591                if let Some(&draw_idx) = draw_indices.get(stream_id) {
592                    ops.record_with(move |backend, out| {
593                        if let Err(e) = backend.upload_mesh(draw_idx, &verts, &idxs, frame) {
594                            tracing::debug!(
595                                "StreamingSystem: mesh upload draw {} deferred: {}",
596                                draw_idx,
597                                e
598                            );
599                            out.memory_pressure |=
600                                matches!(e, crate::gfx::error::RenderError::OutOfDeviceMemory(_));
601                            out.failures.push(OpFailure::MeshUpload { stream_id });
602                        }
603                    });
604                }
605                if let Some(residency) = residency.as_mut() {
606                    residency.note_resident((CHANNEL_MESH, stream_id as u32), true);
607                }
608            });
609            if let Some((resident, pending, unloaded)) = self
610                .heartbeats
611                .mesh
612                .sample(self.frame_count, || streamer.stats())
613            {
614                tracing::info!(
615                    "StreamingSystem: mesh streaming -- {} resident, {} pending, {} unloaded",
616                    resident,
617                    pending,
618                    unloaded
619                );
620            }
621        }
622
623        // Drive infinite-world chunk streaming: generate + upload the chunks
624        // entering the camera's view window and remove those that have left it.
625        // None unless a VoxelWorld was declared.
626        //
627        // Camera-relative rendering: chunk geometry is placed relative to a
628        // render origin that follows the camera's chunk, and the view + camera
629        // position handed to the backend are rebased onto the same origin. The
630        // world transform is unchanged -- it is just evaluated from small
631        // coordinates, so an unbounded world renders without large-coordinate
632        // jitter. The view + camera stay absolute when no VoxelWorld is
633        // streaming, leaving a non-voxel world byte-for-byte unchanged.
634        let mut final_view = view_matrix;
635        let mut final_cam_pos = cam_pos;
636        if let Some(cs) = &mut self.chunk_stream {
637            let camera_chunk = cs.streamer.camera_chunk(cam_pos);
638            let retire_frame = self.frame_count + self.frames_in_flight as u64;
639            // Stage 1 of the RAM valve freezes chunk residency, mirroring the
640            // texture/mesh gate: skipping plan_and_dispatch stops both new
641            // generation and window eviction, so residency holds steady while
642            // the drain below keeps applying in-flight loads. Stage 2 keeps
643            // planning under a reduced byte budget, so the window clamp evicts.
644            if !loads_frozen {
645                for coord in cs.streamer.plan_and_dispatch(camera_chunk) {
646                    if let Some(draw_idx) = cs.draws.remove(&coord) {
647                        slots.free_draw(draw_idx);
648                        ops.record(move |backend| {
649                            if let Err(e) = backend.remove_chunk_mesh(draw_idx, retire_frame) {
650                                tracing::warn!(
651                                    "StreamingSystem: chunk remove ({},{}): {}",
652                                    coord.x,
653                                    coord.z,
654                                    e
655                                );
656                            }
657                        });
658                    }
659                }
660            }
661            // The camera crossed into a new chunk: move the render origin to it
662            // and rebase every resident chunk's model matrix. `prev_draw_models`
663            // is deliberately left alone -- the rebase is exact, so a stationary
664            // chunk shows zero TAA velocity across the shift.
665            if camera_chunk != cs.origin_chunk {
666                for (&coord, &draw_idx) in &cs.draws {
667                    let model = chunk_model_matrix(coord, camera_chunk, cs.chunk_w, cs.chunk_d);
668                    ops.record(move |backend| {
669                        if let Err(e) = backend.set_chunk_model(draw_idx, model) {
670                            tracing::warn!(
671                                "StreamingSystem: chunk rebase ({},{}): {}",
672                                coord.x,
673                                coord.z,
674                                e
675                            );
676                        }
677                    });
678                }
679                cs.origin_chunk = camera_chunk;
680            }
681            let frame = self.frame_count;
682            let (chunk_w, chunk_d) = (cs.chunk_w, cs.chunk_d);
683            let (tex, nm, mat) = (cs.texture_slot, cs.normal_map_slot, cs.material);
684            let mut added: Vec<(crate::gfx::chunk_coord::ChunkCoord, usize)> = Vec::new();
685            cs.streamer.drain_completed(|coord, verts, idxs| {
686                if verts.is_empty() || idxs.is_empty() {
687                    tracing::warn!(
688                        "StreamingSystem: chunk add ({},{}): empty chunk geometry",
689                        coord.x,
690                        coord.z
691                    );
692                    return;
693                }
694                let model = chunk_model_matrix(coord, camera_chunk, chunk_w, chunk_d);
695                let dst = slots.allocate_draw();
696                let draw_idx = match dst {
697                    crate::gfx::draw_slot::SlotAlloc::Reuse(i)
698                    | crate::gfx::draw_slot::SlotAlloc::Append(i) => i,
699                };
700                added.push((coord, draw_idx));
701                // A failed add comes back as an op failure; the rollback drops
702                // the tracking and frees the slot.
703                ops.record_with(move |backend, out| {
704                    let mesh = ChunkMesh {
705                        verts: &verts,
706                        idxs: &idxs,
707                        model,
708                        texture_slot: tex,
709                        normal_map_slot: nm,
710                        material: mat,
711                        frame,
712                    };
713                    if let Err(e) = backend.add_chunk_mesh(mesh, dst) {
714                        tracing::warn!(
715                            "StreamingSystem: chunk add ({},{}): {}",
716                            coord.x,
717                            coord.z,
718                            e
719                        );
720                        out.memory_pressure |=
721                            matches!(e, crate::gfx::error::RenderError::OutOfDeviceMemory(_));
722                        out.failures.push(OpFailure::ChunkAdd { coord });
723                    }
724                });
725            });
726            for (coord, draw_idx) in added {
727                cs.draws.insert(coord, draw_idx);
728            }
729            // Rebase the view + camera onto the render origin so the
730            // origin-relative chunk geometry above transforms exactly.
731            let (ox, oz) = camera_chunk.origin_world(cs.chunk_w, cs.chunk_d);
732            let origin = [ox, 0.0, oz];
733            final_view =
734                crate::gfx::chunk_coord::camera_relative_view(view_matrix, cam_pos, origin);
735            final_cam_pos = [cam_pos[0] - ox, cam_pos[1], cam_pos[2] - oz];
736            if let Some((resident, pending, near, far)) =
737                self.heartbeats.chunk.sample(self.frame_count, || {
738                    let (resident, pending) = cs.streamer.stats();
739                    let (near, far) = cs.streamer.detail_counts();
740                    (resident, pending, near, far)
741                })
742            {
743                tracing::info!(
744                    "StreamingSystem: chunk streaming -- {} resident ({} full, {} impostor), {} pending",
745                    resident,
746                    near,
747                    far,
748                    pending
749                );
750            }
751        }
752
753        self.frame_count += 1;
754        (final_view, final_cam_pos)
755    }
756
757    // Re-evaluate the process-RAM back-off valve from a fresh RSS sample and
758    // apply its decision to the texture + mesh pools. Returns the pressure
759    // reading to publish, or `None` when RSS is unavailable (the valve stays
760    // inert and nothing is published). `budget` is the `MemoryBudget` ceiling.
761    fn sample_pressure(&mut self, rss: Option<u64>, budget: u64) -> Option<StreamingPressure> {
762        let rss = rss?;
763        let rising = self.last_sampled_rss.is_some_and(|prev| rss > prev);
764        let prev_stage = self.pressure_stage;
765        let decision =
766            pressure::step_pressure(rss, budget, rising, prev_stage, self.pressure_factor);
767
768        // Re-apply the pool byte budgets only when the reduced-budget state
769        // actually needs it: while evicting (the factor may have tightened) or
770        // on the transition out of eviction (restore the baseline exactly).
771        // Staying at None/Gate leaves the budgets at their baseline untouched,
772        // so a world never under pressure behaves exactly as before.
773        use pressure::StreamPressureStage::Evict;
774        match (prev_stage, decision.stage) {
775            (_, Evict) => self.apply_byte_factor(decision.budget_factor),
776            (Evict, _) => self.apply_byte_factor(1.0),
777            _ => {}
778        }
779
780        self.pressure_stage = decision.stage;
781        self.pressure_factor = decision.budget_factor;
782        self.last_sampled_rss = Some(rss);
783        Some(StreamingPressure {
784            rss_bytes: rss,
785            budget_bytes: budget,
786            under_pressure: decision.stage != pressure::StreamPressureStage::None,
787        })
788    }
789
790    // Fold the same RSS sample into the long-session drift tracker, stating the
791    // reading whenever it changes. Reports only: every valve stage is decided
792    // by `sample_pressure`, and nothing here moves a byte budget.
793    fn sample_drift(
794        &mut self,
795        rss: Option<u64>,
796        budget: u64,
797    ) -> Option<crate::app::mem_drift::MemoryDrift> {
798        use crate::app::mem_drift::DriftVerdict;
799        let rss = rss?;
800        let heap_live = concinnity_core::memory::stats()?.live_bytes;
801        let drift = self.drift.sample(rss, heap_live, budget)?;
802
803        if self.last_drift_verdict != Some(drift.verdict) {
804            self.last_drift_verdict = Some(drift.verdict);
805            let heap_mib = drift.heap_growth_bytes / (1024 * 1024);
806            let outside_mib = drift.outside_heap_growth_bytes / (1024 * 1024);
807            let minutes = drift.window_secs / 60;
808            let reading = drift.verdict.label();
809            if drift.verdict == DriftVerdict::Settled {
810                tracing::info!(
811                    "memory drift: {reading} -- heap {heap_mib:+} MiB, outside heap {outside_mib:+} MiB over {minutes} min"
812                );
813            } else {
814                tracing::warn!(
815                    "memory drift: {reading} -- heap {heap_mib:+} MiB, outside heap {outside_mib:+} MiB over {minutes} min"
816                );
817            }
818        }
819        Some(drift)
820    }
821
822    // Scale each pool's byte budget to `factor` of its captured baseline. Pools
823    // with no baseline (count-only) are left alone; there is nothing to reduce.
824    fn apply_byte_factor(&mut self, factor: f64) {
825        if let (Some(streamer), Some(baseline)) =
826            (self.texture_streamer.as_mut(), self.texture_baseline_budget)
827        {
828            streamer.set_byte_budget(Some(pressure::scale_budget(baseline, factor)));
829        }
830        if let (Some(streamer), Some(baseline)) =
831            (self.mesh_streamer.as_mut(), self.mesh_baseline_budget)
832        {
833            streamer.set_byte_budget(Some(pressure::scale_budget(baseline, factor)));
834        }
835        // The chunk pool's byte clamp responds to a reduced budget by shrinking
836        // its effective view radius, dropping the far impostor band first.
837        if let (Some(cs), Some(baseline)) = (self.chunk_stream.as_mut(), self.chunk_baseline_budget)
838        {
839            cs.streamer
840                .set_byte_budget(Some(pressure::scale_budget(baseline, factor)));
841        }
842    }
843
844    // What each streaming pool holds in device memory, for the shared ledger.
845    // Only pools that are actually streaming report.
846    //
847    // `transient_pool_bytes` is the render graph's transient pool, which is not a
848    // streaming pool at all: it sits off the device allocator (its slots alias on
849    // purpose, which the general allocator must never do) and so is invisible to
850    // every other accounting path. It rides in under `Textures` because that is
851    // what it holds. Its bytes are added to the budget as well as the usage, so
852    // the streamer's own headroom against its own cap is unchanged -- the pool is
853    // not competing for the streamer's budget, it is reporting alongside it.
854    fn pool_reports(
855        &self,
856        transient_pool_bytes: u64,
857    ) -> impl Iterator<Item = accounting::PoolReport> {
858        let textures = accounting::textures_report(
859            self.texture_streamer
860                .as_ref()
861                .map(|s| (s.resident_bytes(), s.byte_budget())),
862            transient_pool_bytes,
863        );
864        [
865            self.mesh_streamer.as_ref().map(|s| {
866                (
867                    concinnity_core::memory::MemTag::Meshes,
868                    s.resident_bytes(),
869                    s.byte_budget(),
870                )
871            }),
872            self.chunk_stream.as_ref().map(|cs| {
873                (
874                    concinnity_core::memory::MemTag::Chunks,
875                    cs.streamer.resident_bytes(),
876                    cs.streamer.byte_budget(),
877                )
878            }),
879        ]
880        .into_iter()
881        .flatten()
882        .map(
883            |(tag, resident_bytes, byte_budget)| accounting::PoolReport {
884                tag,
885                resident_bytes,
886                byte_budget,
887            },
888        )
889        .chain(textures)
890    }
891
892    // `(resident, pending, unloaded)` counts for each active streaming pool.
893    // Consumed only by the `cn debug` binary's `streaming` command, so it reads
894    // as dead code in a plain library build.
895    pub(crate) fn streaming_stats(&self) -> StreamingStats {
896        StreamingStats {
897            texture: self.texture_streamer.as_ref().map(|s| s.stats()),
898            mesh: self.mesh_streamer.as_ref().map(|s| s.stats()),
899            chunk: self.chunk_stream.as_ref().map(|cs| cs.streamer.stats()),
900            texture_bytes: self
901                .texture_streamer
902                .as_ref()
903                .map(|s| (s.resident_bytes(), s.byte_budget().unwrap_or(0))),
904            mesh_bytes: self
905                .mesh_streamer
906                .as_ref()
907                .map(|s| (s.resident_bytes(), s.byte_budget().unwrap_or(0))),
908            chunk_bytes: self.chunk_stream.as_ref().map(|cs| {
909                (
910                    cs.streamer.resident_bytes(),
911                    cs.streamer.byte_budget().unwrap_or(0),
912                )
913            }),
914        }
915    }
916}
917
918// Model matrix that places chunk `coord`'s origin-local geometry relative to
919// the render origin `origin`, so the on-GPU transform stays exact and small
920// regardless of how far the world origin is. The matching view matrix is
921// rebased onto the same origin by `camera_relative_view`, which keeps an
922// unbounded world's precision intact.
923pub(crate) fn chunk_model_matrix(
924    coord: crate::gfx::chunk_coord::ChunkCoord,
925    origin: crate::gfx::chunk_coord::ChunkCoord,
926    chunk_w: f32,
927    chunk_d: f32,
928) -> [[f32; 4]; 4] {
929    let dx = (coord.x - origin.x) as f32 * chunk_w;
930    let dz = (coord.z - origin.z) as f32 * chunk_d;
931    [
932        [1.0, 0.0, 0.0, 0.0],
933        [0.0, 1.0, 0.0, 0.0],
934        [0.0, 0.0, 1.0, 0.0],
935        [dx, 0.0, dz, 1.0],
936    ]
937}
938
939#[cfg(test)]
940mod tests {
941    use super::*;
942    use crate::blob::BlobData;
943    use crate::ecs::{ComponentStorage, Resources};
944    use crate::gfx::chunk_coord::ChunkCoord;
945    use crate::gfx::chunk_window::ChunkDetail;
946    use crate::gfx::mesh_payload::Vertex;
947    use crate::gfx::mock_backend::{Call, MockBackend, recording_backend};
948    use crate::gfx::profile::FrameProfile;
949    use crate::gfx::streaming::chunk::{ChunkSource, ChunkStreamer};
950    use crate::gfx::streaming::mesh::{DecodedMesh, MeshPayloadSource, MeshStreamer};
951    use crate::gfx::streaming::texture::{DecodedTexture, PayloadSource, TextureStreamer};
952    use pressure::StreamPressureStage;
953    use std::sync::Arc;
954
955    // Upper bound on `drive_until` iterations. The pools decode on their own
956    // worker threads, so a test that waits on one yields rather than sleeps;
957    // the bound turns a regression into a failure instead of a hang.
958    const MAX_DRIVE_SPINS: usize = 100_000;
959
960    fn vtx() -> Vertex {
961        Vertex {
962            pos: [0.0; 3],
963            normal: [0.0, 1.0, 0.0],
964            tangent: [1.0, 0.0, 0.0],
965            color: [1.0; 3],
966            uv: [0.0; 2],
967        }
968    }
969
970    fn tri() -> DecodedMesh {
971        DecodedMesh {
972            vertices: vec![vtx(), vtx(), vtx()],
973            indices: vec![0, 1, 2],
974        }
975    }
976
977    // Sources yielding a fixed payload for any id, so the streamer workers
978    // complete without the build pipeline.
979    struct ConstTexture;
980    impl PayloadSource for ConstTexture {
981        fn fetch(&self, _id: usize) -> Result<DecodedTexture, String> {
982            Ok(DecodedTexture {
983                image: crate::bake::texture::TextureImage::rgba8(1, 1, vec![1, 2, 3, 4]),
984            })
985        }
986    }
987
988    struct ConstMesh;
989    impl MeshPayloadSource for ConstMesh {
990        fn fetch(&self, _id: usize) -> Result<DecodedMesh, String> {
991            Ok(tri())
992        }
993    }
994
995    struct ConstChunk;
996    impl ChunkSource for ConstChunk {
997        fn generate(
998            &self,
999            _coord: ChunkCoord,
1000            _detail: ChunkDetail,
1001        ) -> Result<DecodedMesh, String> {
1002            Ok(tri())
1003        }
1004    }
1005
1006    // A source whose generation always fails, so a chunk is tracked by the
1007    // window but never uploaded: it keeps the resident-draw map under the
1008    // test's control rather than the worker's timing.
1009    struct FailingChunk;
1010    impl ChunkSource for FailingChunk {
1011        fn generate(
1012            &self,
1013            _coord: ChunkCoord,
1014            _detail: ChunkDetail,
1015        ) -> Result<DecodedMesh, String> {
1016            Err("test source".to_string())
1017        }
1018    }
1019
1020    // A view matrix that is a pure translation: enough to tell a rebased view
1021    // apart from the absolute one it was derived from.
1022    fn translation_view(x: f32, y: f32, z: f32) -> [[f32; 4]; 4] {
1023        [
1024            [1.0, 0.0, 0.0, 0.0],
1025            [0.0, 1.0, 0.0, 0.0],
1026            [0.0, 0.0, 1.0, 0.0],
1027            [x, y, z, 1.0],
1028        ]
1029    }
1030
1031    // A bare StreamingState with no pools: enough to exercise the RAM valve's
1032    // sampling + stage machine without standing up the streamer worker threads.
1033    // `apply_byte_factor` is a no-op with no baselines, so the transitions run
1034    // exactly as they would with pools attached.
1035    fn empty_state() -> StreamingState {
1036        StreamingState {
1037            texture_streamer: None,
1038            mesh_streamer: None,
1039            mesh_stream_draw_indices: Vec::new(),
1040            chunk_stream: None,
1041            shader_warmup: None,
1042            scene_residency: None,
1043            frame_count: 0,
1044            frames_in_flight: 2,
1045            texture_baseline_budget: None,
1046            mesh_baseline_budget: None,
1047            chunk_baseline_budget: None,
1048            pressure_stage: StreamPressureStage::None,
1049            pressure_factor: 1.0,
1050            last_sampled_rss: None,
1051            drift: Default::default(),
1052            last_drift_verdict: None,
1053            heartbeats: Default::default(),
1054        }
1055    }
1056
1057    // Texture + mesh pools of two items each: item 0 sits on the camera's
1058    // origin and item 1 far out on +X, so a camera at either end orders the two
1059    // unambiguously. `resident_cap` chooses whether both fit at once (8) or the
1060    // second must displace the first (1). Mesh stream ids map to draw slots
1061    // 10 / 11 so an upload's routing is visible in the call log.
1062    fn pooled_state(resident_cap: usize) -> StreamingState {
1063        let centers = vec![vec![[0.0, 0.0, 0.0]], vec![[100.0, 0.0, 0.0]]];
1064        let mut state = empty_state();
1065        state.texture_streamer = Some(TextureStreamer::new(
1066            Arc::new(ConstTexture),
1067            centers.clone(),
1068            4,
1069            resident_cap,
1070        ));
1071        state.mesh_streamer = Some(MeshStreamer::new(
1072            Arc::new(ConstMesh),
1073            centers,
1074            4,
1075            resident_cap,
1076        ));
1077        state.mesh_stream_draw_indices = vec![10, 11];
1078        state
1079    }
1080
1081    // A chunk pool over 16x16 chunks at the given near / far radii.
1082    fn chunk_state(source: Arc<dyn ChunkSource>, near: i32, far: i32) -> ChunkStreamState {
1083        ChunkStreamState {
1084            streamer: ChunkStreamer::new(source, near, far, 64, 16.0, 16.0),
1085            draws: std::collections::BTreeMap::new(),
1086            chunk_w: 16.0,
1087            chunk_d: 16.0,
1088            origin_chunk: ChunkCoord::new(0, 0),
1089            texture_slot: 0,
1090            normal_map_slot: crate::gfx::render_types::NO_NORMAL_MAP_SLOT,
1091            material: crate::gfx::render_types::MaterialUniforms::DEFAULT,
1092        }
1093    }
1094
1095    // Drive frames until `done` holds. Yields (never sleeps) between frames
1096    // while the pools' workers decode; panics rather than hanging if the state
1097    // is never reached.
1098    // One drive with a throwaway op queue + slot allocator, replaying the
1099    // recorded ops onto the mock backend, for tests that assert per-frame
1100    // behavior rather than the multi-frame pump `drive_until` covers.
1101    fn drive_once(
1102        state: &mut StreamingState,
1103        backend: &mut MockBackend,
1104        cam: [f32; 3],
1105        view: [[f32; 4]; 4],
1106        world_hidden: bool,
1107        pins: Option<&[AssetId]>,
1108    ) -> ([[f32; 4]; 4], [f32; 3]) {
1109        let mut slots = RenderSlots::new(0, true, &[]);
1110        let mut ops = RenderOps::default();
1111        let out = state.drive(&mut ops, &mut slots, cam, view, world_hidden, pins);
1112        let outcome = ops.replay(backend);
1113        state.apply_op_failures(&outcome.failures, &mut slots);
1114        out
1115    }
1116
1117    fn drive_until(
1118        state: &mut StreamingState,
1119        backend: &mut MockBackend,
1120        cam: [f32; 3],
1121        done: impl Fn(&StreamingState) -> bool,
1122    ) {
1123        let mut slots = RenderSlots::new(0, true, &[]);
1124        for _ in 0..MAX_DRIVE_SPINS {
1125            let mut ops = RenderOps::default();
1126            state.drive(&mut ops, &mut slots, cam, IDENTITY4, false, None);
1127            let outcome = ops.replay(backend);
1128            state.apply_op_failures(&outcome.failures, &mut slots);
1129            if done(state) {
1130                return;
1131            }
1132            std::thread::yield_now();
1133        }
1134        panic!("streaming never reached the expected state");
1135    }
1136
1137    // Owns the storage a PipelineContext borrows from, for the `step` tests.
1138    struct StepWorld {
1139        components: ComponentStorage,
1140        blob: BlobData,
1141        profile: FrameProfile,
1142        resources: Resources,
1143        scratch: crate::ecs::Arena,
1144    }
1145
1146    impl StepWorld {
1147        fn new() -> Self {
1148            Self {
1149                components: ComponentStorage::default(),
1150                blob: BlobData::empty(),
1151                profile: FrameProfile::default(),
1152                resources: Resources::new(),
1153                scratch: crate::ecs::Arena::with_capacity(64 * 1024),
1154            }
1155        }
1156
1157        // Place a camera the step will read the absolute view + position from.
1158        fn with_camera(mut self, position: [f32; 3], view_matrix: [[f32; 4]; 4]) -> Self {
1159            self.components.push_typed(Camera3D {
1160                position,
1161                view_matrix,
1162                ..Camera3D::bake(Default::default())
1163            });
1164            self
1165        }
1166
1167        // Publish the op queue + slot allocator the step takes and reparks
1168        // (the pair graphics init publishes in production).
1169        fn park_render_queues(&mut self) {
1170            self.resources.insert(crate::ecs::ActiveRenderQueues(Some(
1171                crate::ecs::RenderQueues {
1172                    ops: Default::default(),
1173                    slots: RenderSlots::new(0, true, &[]),
1174                },
1175            )));
1176        }
1177
1178        fn step(&mut self) -> StepResult {
1179            let mut ctx = PipelineContext {
1180                components: &mut self.components,
1181                blob: &mut self.blob,
1182                profile: &mut self.profile,
1183                resources: &mut self.resources,
1184                frame: crate::ecs::FrameContext::new(&self.scratch),
1185            };
1186            StreamingSystem::new().step(&mut ctx)
1187        }
1188
1189        fn view(&self) -> CameraRelativeView {
1190            *self
1191                .resources
1192                .get::<CameraRelativeView>()
1193                .expect("camera-relative view published")
1194        }
1195
1196        fn parked_state(&self) -> &StreamingState {
1197            self.resources
1198                .get::<StreamingState>()
1199                .expect("state parked again")
1200        }
1201    }
1202
1203    #[test]
1204    fn sample_pressure_engages_and_publishes() {
1205        let mut s = empty_state();
1206        // RSS at 92% of a 1000-byte budget: stage 1 engages.
1207        let p = s.sample_pressure(Some(920), 1000).expect("published");
1208        assert_eq!(s.pressure_stage, StreamPressureStage::Gate);
1209        assert!(p.under_pressure);
1210        assert_eq!(p.rss_bytes, 920);
1211        assert_eq!(p.budget_bytes, 1000);
1212    }
1213
1214    // The drift tracker rides the same sample as the valve: it reports nothing
1215    // until the session settles, and the two terms it then reports always
1216    // account for exactly the RSS movement between them.
1217    #[test]
1218    fn sample_drift_reports_once_settled_and_splits_the_whole_rss_movement() {
1219        const RSS: u64 = 2 * 1024 * 1024 * 1024;
1220        const BUDGET: u64 = 4 * RSS;
1221        let mut s = empty_state();
1222
1223        assert_eq!(s.sample_drift(None, BUDGET), None, "no RSS, no drift");
1224        // Steady samples until the tracker settles and starts reporting; the
1225        // run it needs is the drift module's business, not this system's.
1226        let d = (0..16)
1227            .find_map(|_| s.sample_drift(Some(RSS), BUDGET))
1228            .expect("a steady run settles the baseline");
1229        assert_eq!(d.verdict, crate::app::mem_drift::DriftVerdict::Settled);
1230        // RSS did not move, so whatever the heap did, the outside term is its
1231        // exact complement.
1232        assert_eq!(d.heap_growth_bytes + d.outside_heap_growth_bytes, 0);
1233    }
1234
1235    // Drift is reported, never acted on: the valve's stage and byte-budget
1236    // factor are decided entirely by `sample_pressure`.
1237    #[test]
1238    fn sample_drift_never_moves_the_valve() {
1239        const RSS: u64 = 2 * 1024 * 1024 * 1024;
1240        let mut s = empty_state();
1241        for _ in 0..4 {
1242            s.sample_drift(Some(RSS), 4 * RSS);
1243        }
1244        assert_eq!(s.pressure_stage, StreamPressureStage::None);
1245        assert_eq!(s.pressure_factor, 1.0);
1246        assert_eq!(s.last_sampled_rss, None);
1247    }
1248
1249    #[test]
1250    fn sample_pressure_escalates_when_rss_keeps_rising() {
1251        let mut s = empty_state();
1252        s.sample_pressure(Some(910), 1000);
1253        assert_eq!(s.pressure_stage, StreamPressureStage::Gate);
1254        // Still above engage and climbing: escalate to eviction.
1255        s.sample_pressure(Some(925), 1000);
1256        assert_eq!(s.pressure_stage, StreamPressureStage::Evict);
1257        assert!(s.pressure_factor < 1.0);
1258    }
1259
1260    #[test]
1261    fn sample_pressure_releases_with_hysteresis() {
1262        let mut s = empty_state();
1263        s.sample_pressure(Some(970), 1000); // straight to evict
1264        assert_eq!(s.pressure_stage, StreamPressureStage::Evict);
1265        // In the hysteresis band (85%): still latched.
1266        s.sample_pressure(Some(850), 1000);
1267        assert_eq!(s.pressure_stage, StreamPressureStage::Evict);
1268        // Below the release mark: valve releases and restores the baseline.
1269        let p = s.sample_pressure(Some(700), 1000).expect("published");
1270        assert_eq!(s.pressure_stage, StreamPressureStage::None);
1271        assert_eq!(s.pressure_factor, 1.0);
1272        assert!(!p.under_pressure);
1273    }
1274
1275    #[test]
1276    fn sample_pressure_is_inert_without_rss() {
1277        let mut s = empty_state();
1278        s.sample_pressure(Some(970), 1000);
1279        let stage_before = s.pressure_stage;
1280        // A failed RSS query publishes nothing and leaves the stage untouched.
1281        assert!(s.sample_pressure(None, 1000).is_none());
1282        assert_eq!(s.pressure_stage, stage_before);
1283    }
1284
1285    // The translation column is the integer chunk delta scaled by chunk size;
1286    // the basis stays identity.
1287    #[test]
1288    fn chunk_model_matrix_offsets_by_chunk_delta() {
1289        let m = chunk_model_matrix(ChunkCoord::new(2, -3), ChunkCoord::new(0, 0), 16.0, 10.0);
1290        assert_eq!(m[3], [32.0, 0.0, -30.0, 1.0]);
1291        assert_eq!(m[0], [1.0, 0.0, 0.0, 0.0]);
1292        assert_eq!(m[1], [0.0, 1.0, 0.0, 0.0]);
1293        assert_eq!(m[2], [0.0, 0.0, 1.0, 0.0]);
1294    }
1295
1296    #[test]
1297    fn chunk_model_matrix_origin_chunk_is_untranslated() {
1298        let c = ChunkCoord::new(5, 7);
1299        let m = chunk_model_matrix(c, c, 16.0, 16.0);
1300        assert_eq!(m[3], [0.0, 0.0, 0.0, 1.0]);
1301    }
1302
1303    // Deep pressure scales every pool's budget off its own captured baseline,
1304    // and releasing restores each one exactly (not merely approximately).
1305    #[test]
1306    fn deep_pressure_scales_each_pool_budget_and_release_restores_the_baseline() {
1307        let mut state = pooled_state(8);
1308        state.chunk_stream = Some(chunk_state(Arc::new(ConstChunk), 0, 0));
1309        state.texture_baseline_budget = Some(4000);
1310        state.mesh_baseline_budget = Some(2000);
1311        state.chunk_baseline_budget = Some(1000);
1312        state.apply_byte_factor(1.0);
1313
1314        state.sample_pressure(Some(970), 1000);
1315        assert_eq!(state.pressure_stage, StreamPressureStage::Evict);
1316        let factor = state.pressure_factor;
1317        assert!(factor < 1.0);
1318        let tex = state.texture_streamer.as_ref().unwrap().byte_budget();
1319        let mesh = state.mesh_streamer.as_ref().unwrap().byte_budget();
1320        let chunk = state.chunk_stream.as_ref().unwrap().streamer.byte_budget();
1321        assert_eq!(tex, Some(pressure::scale_budget(4000, factor)));
1322        assert_eq!(mesh, Some(pressure::scale_budget(2000, factor)));
1323        assert_eq!(chunk, Some(pressure::scale_budget(1000, factor)));
1324
1325        state.sample_pressure(Some(100), 1000);
1326        assert_eq!(state.pressure_stage, StreamPressureStage::None);
1327        assert_eq!(
1328            state.texture_streamer.as_ref().unwrap().byte_budget(),
1329            Some(4000)
1330        );
1331        assert_eq!(
1332            state.mesh_streamer.as_ref().unwrap().byte_budget(),
1333            Some(2000)
1334        );
1335        assert_eq!(
1336            state.chunk_stream.as_ref().unwrap().streamer.byte_budget(),
1337            Some(1000)
1338        );
1339    }
1340
1341    // A pool with no captured baseline runs count-only: the valve has nothing
1342    // to scale, so it must not invent a budget for it.
1343    #[test]
1344    fn count_only_pools_gain_no_byte_budget_under_pressure() {
1345        let mut state = pooled_state(8);
1346        state.chunk_stream = Some(chunk_state(Arc::new(ConstChunk), 0, 0));
1347
1348        state.sample_pressure(Some(970), 1000);
1349        assert_eq!(state.pressure_stage, StreamPressureStage::Evict);
1350        assert_eq!(state.texture_streamer.as_ref().unwrap().byte_budget(), None);
1351        assert_eq!(state.mesh_streamer.as_ref().unwrap().byte_budget(), None);
1352        assert_eq!(
1353            state.chunk_stream.as_ref().unwrap().streamer.byte_budget(),
1354            None
1355        );
1356    }
1357
1358    #[test]
1359    fn streaming_stats_reports_every_active_pool() {
1360        let mut state = pooled_state(8);
1361        state.chunk_stream = Some(chunk_state(Arc::new(ConstChunk), 0, 0));
1362        state
1363            .texture_streamer
1364            .as_mut()
1365            .unwrap()
1366            .set_byte_budget(Some(4000));
1367
1368        let stats = state.streaming_stats();
1369        assert_eq!(stats.texture, Some((0, 0, 2)));
1370        assert_eq!(stats.mesh, Some((0, 0, 2)));
1371        assert_eq!(stats.chunk, Some((0, 0)));
1372        assert_eq!(stats.texture_bytes, Some((0, 4000)));
1373        // A count-only pool reports a zero budget rather than dropping the row.
1374        assert_eq!(stats.mesh_bytes, Some((0, 0)));
1375        assert_eq!(stats.chunk_bytes, Some((0, 0)));
1376    }
1377
1378    #[test]
1379    fn streaming_stats_reports_nothing_without_pools() {
1380        let stats = empty_state().streaming_stats();
1381        assert!(stats.texture.is_none());
1382        assert!(stats.mesh.is_none());
1383        assert!(stats.chunk.is_none());
1384        assert!(stats.texture_bytes.is_none());
1385        assert!(stats.mesh_bytes.is_none());
1386        assert!(stats.chunk_bytes.is_none());
1387    }
1388
1389    // The pools have no Debug of their own, so StreamingState's is hand-written
1390    // to report which are active rather than trying to dump them.
1391    #[test]
1392    fn debug_reports_the_active_pools_and_the_frame_clock() {
1393        let mut state = pooled_state(8);
1394        state.frame_count = 7;
1395        state.pressure_stage = StreamPressureStage::Gate;
1396
1397        let s = format!("{state:?}");
1398        assert!(s.contains("frame_count: 7"), "{s}");
1399        assert!(s.contains("texture: true"), "{s}");
1400        assert!(s.contains("mesh: true"), "{s}");
1401        assert!(s.contains("chunk: false"), "{s}");
1402        assert!(s.contains("pressure: Gate"), "{s}");
1403    }
1404
1405    #[test]
1406    fn drive_advances_the_frame_clock() {
1407        let (_recorded, mut backend) = recording_backend();
1408        let mut state = empty_state();
1409        drive_once(&mut state, &mut backend, [0.0; 3], IDENTITY4, false, None);
1410        drive_once(&mut state, &mut backend, [0.0; 3], IDENTITY4, false, None);
1411        assert_eq!(state.frame_count, 2);
1412    }
1413
1414    // Behind an opaque menu the world is not drawn, so no pool dispatches: both
1415    // stay fully unloaded rather than paying for loads nothing will show.
1416    #[test]
1417    fn a_hidden_world_dispatches_no_loads() {
1418        let (_recorded, mut backend) = recording_backend();
1419        let mut state = pooled_state(8);
1420        drive_once(&mut state, &mut backend, [0.0; 3], IDENTITY4, true, None);
1421        assert_eq!(state.texture_streamer.as_ref().unwrap().stats(), (0, 0, 2));
1422        assert_eq!(state.mesh_streamer.as_ref().unwrap().stats(), (0, 0, 2));
1423    }
1424
1425    #[test]
1426    fn a_visible_world_dispatches_texture_and_mesh_loads() {
1427        let (_recorded, mut backend) = recording_backend();
1428        let mut state = pooled_state(8);
1429        drive_once(&mut state, &mut backend, [0.0; 3], IDENTITY4, false, None);
1430        // Dispatch moves an item off Unloaded the same frame it is planned.
1431        assert!(state.texture_streamer.as_ref().unwrap().stats().2 < 2);
1432        assert!(state.mesh_streamer.as_ref().unwrap().stats().2 < 2);
1433    }
1434
1435    // Scene residency over the pools: only the pinned scene's members stream;
1436    // switching the pin set drains the old scene and loads the new one.
1437    #[test]
1438    fn scene_residency_streams_only_the_pinned_scene_and_swaps_on_switch() {
1439        use crate::gfx::scene_residency::SceneLoadState;
1440
1441        let (_recorded, mut backend) = recording_backend();
1442        let mut state = pooled_state(8);
1443        let scene_a = AssetId(70);
1444        let scene_b = AssetId(71);
1445        let residency = SceneResidency::new(vec![
1446            (scene_a, vec![(CHANNEL_TEXTURE, 0), (CHANNEL_MESH, 0)]),
1447            (scene_b, vec![(CHANNEL_TEXTURE, 1), (CHANNEL_MESH, 1)]),
1448        ]);
1449        // Mirror init: every owned member starts blocked.
1450        for (channel, id) in residency.all_members().collect::<Vec<_>>() {
1451            match channel {
1452                CHANNEL_TEXTURE => state
1453                    .texture_streamer
1454                    .as_mut()
1455                    .unwrap()
1456                    .set_blocked(id as usize, true),
1457                _ => state
1458                    .mesh_streamer
1459                    .as_mut()
1460                    .unwrap()
1461                    .set_blocked(id as usize, true),
1462            }
1463        }
1464        state.scene_residency = Some(residency);
1465
1466        // Pin scene A: its members stream in; B's stay blocked out.
1467        let pins_a = [scene_a];
1468        for _ in 0..MAX_DRIVE_SPINS {
1469            drive_once(
1470                &mut state,
1471                &mut backend,
1472                [0.0; 3],
1473                IDENTITY4,
1474                false,
1475                Some(&pins_a),
1476            );
1477            let r = state.scene_residency.as_ref().unwrap();
1478            if r.state(scene_a) == Some(SceneLoadState::Resident) {
1479                break;
1480            }
1481            std::thread::yield_now();
1482        }
1483        let r = state.scene_residency.as_ref().unwrap();
1484        assert_eq!(r.state(scene_a), Some(SceneLoadState::Resident));
1485        assert_eq!(r.state(scene_b), Some(SceneLoadState::Unloaded));
1486        assert_eq!(
1487            state.texture_streamer.as_ref().unwrap().stats().0,
1488            1,
1489            "only A's texture is resident"
1490        );
1491
1492        // Switch the pin to scene B: A drains off the GPU, B streams in.
1493        let pins_b = [scene_b];
1494        for _ in 0..MAX_DRIVE_SPINS {
1495            drive_once(
1496                &mut state,
1497                &mut backend,
1498                [0.0; 3],
1499                IDENTITY4,
1500                false,
1501                Some(&pins_b),
1502            );
1503            let r = state.scene_residency.as_ref().unwrap();
1504            if r.state(scene_b) == Some(SceneLoadState::Resident)
1505                && r.state(scene_a) == Some(SceneLoadState::Unloaded)
1506            {
1507                break;
1508            }
1509            std::thread::yield_now();
1510        }
1511        let r = state.scene_residency.as_ref().unwrap();
1512        assert_eq!(r.state(scene_b), Some(SceneLoadState::Resident));
1513        assert_eq!(r.progress(scene_b), Some(1.0));
1514        assert_eq!(r.state(scene_a), Some(SceneLoadState::Unloaded));
1515        assert_eq!(state.texture_streamer.as_ref().unwrap().stats().0, 1);
1516        assert_eq!(state.mesh_streamer.as_ref().unwrap().stats().0, 1);
1517    }
1518
1519    // Stage 1 of the RAM valve holds residency where it is: no new dispatch.
1520    #[test]
1521    fn the_gate_stage_freezes_new_loads() {
1522        let (_recorded, mut backend) = recording_backend();
1523        let mut state = pooled_state(8);
1524        state.chunk_stream = Some(chunk_state(Arc::new(ConstChunk), 0, 0));
1525        state.pressure_stage = StreamPressureStage::Gate;
1526
1527        drive_once(&mut state, &mut backend, [0.0; 3], IDENTITY4, false, None);
1528        assert_eq!(state.texture_streamer.as_ref().unwrap().stats(), (0, 0, 2));
1529        assert_eq!(state.mesh_streamer.as_ref().unwrap().stats(), (0, 0, 2));
1530        assert_eq!(
1531            state.chunk_stream.as_ref().unwrap().streamer.stats(),
1532            (0, 0)
1533        );
1534    }
1535
1536    // Stage 2 keeps planning under the reduced budget, so the planner can still
1537    // shed residents; freezing it instead would strand the pools over budget.
1538    #[test]
1539    fn the_evict_stage_keeps_planning() {
1540        let (_recorded, mut backend) = recording_backend();
1541        let mut state = pooled_state(8);
1542        state.pressure_stage = StreamPressureStage::Evict;
1543        drive_once(&mut state, &mut backend, [0.0; 3], IDENTITY4, false, None);
1544        assert!(state.texture_streamer.as_ref().unwrap().stats().2 < 2);
1545        assert!(state.mesh_streamer.as_ref().unwrap().stats().2 < 2);
1546    }
1547
1548    // Completed loads route to the backend: a texture by pool slot, a mesh
1549    // through its stream-id -> draw-slot map.
1550    #[test]
1551    fn completed_loads_are_uploaded_to_the_backend() {
1552        let (recorded, mut backend) = recording_backend();
1553        let mut state = pooled_state(8);
1554        drive_until(&mut state, &mut backend, [0.0; 3], |s| {
1555            s.texture_streamer.as_ref().unwrap().stats().0 == 2
1556                && s.mesh_streamer.as_ref().unwrap().stats().0 == 2
1557        });
1558
1559        let s = recorded.lock().unwrap();
1560        assert!(s.saw(&Call::UpdateTextureSlot {
1561            slot: 0,
1562            w: 1,
1563            h: 1
1564        }));
1565        assert!(s.saw(&Call::UpdateTextureSlot {
1566            slot: 1,
1567            w: 1,
1568            h: 1
1569        }));
1570        assert!(s.saw(&Call::UploadMesh {
1571            draw_idx: 10,
1572            vertices: 3,
1573            indices: 3,
1574        }));
1575        assert!(s.saw(&Call::UploadMesh {
1576            draw_idx: 11,
1577            vertices: 3,
1578            indices: 3,
1579        }));
1580    }
1581
1582    // A streamed mesh with no draw slot has nowhere to upload to; the drive
1583    // must swallow it rather than mis-routing the geometry onto another draw.
1584    #[test]
1585    fn a_streamed_mesh_without_a_draw_slot_uploads_nothing() {
1586        let (recorded, mut backend) = recording_backend();
1587        let mut state = pooled_state(8);
1588        state.mesh_stream_draw_indices.clear();
1589        drive_until(&mut state, &mut backend, [0.0; 3], |s| {
1590            s.mesh_streamer.as_ref().unwrap().stats().0 == 2
1591        });
1592        assert!(
1593            !recorded
1594                .lock()
1595                .unwrap()
1596                .calls
1597                .iter()
1598                .any(|c| matches!(c, Call::UploadMesh { .. }))
1599        );
1600    }
1601
1602    // Over the resident cap, the pools shed the item the camera has left
1603    // behind so the nearer one can take its place.
1604    #[test]
1605    fn moving_the_camera_evicts_the_now_distant_item_over_the_cap() {
1606        let (recorded, mut backend) = recording_backend();
1607        // Cap of 1: only one texture / mesh may be resident at a time.
1608        let mut state = pooled_state(1);
1609        drive_until(&mut state, &mut backend, [0.0; 3], |s| {
1610            s.texture_streamer.as_ref().unwrap().stats().0 == 1
1611                && s.mesh_streamer.as_ref().unwrap().stats().0 == 1
1612        });
1613        recorded.lock().unwrap().calls.clear();
1614
1615        // Item 1's center is now the near one, so item 0 is displaced.
1616        drive_once(
1617            &mut state,
1618            &mut backend,
1619            [100.0, 0.0, 0.0],
1620            IDENTITY4,
1621            false,
1622            None,
1623        );
1624        let s = recorded.lock().unwrap();
1625        assert!(s.saw(&Call::EvictTextureSlot(0)), "{:?}", s.calls);
1626        assert!(s.saw(&Call::EvictMesh(10)), "{:?}", s.calls);
1627    }
1628
1629    #[test]
1630    fn without_chunk_streaming_the_view_and_camera_stay_absolute() {
1631        let (_recorded, mut backend) = recording_backend();
1632        let mut state = empty_state();
1633        let view = translation_view(-40.0, -5.0, 40.0);
1634        let cam = [40.0, 5.0, -40.0];
1635        assert_eq!(
1636            drive_once(&mut state, &mut backend, cam, view, false, None),
1637            (view, cam)
1638        );
1639    }
1640
1641    // An unbounded world renders from small coordinates: both the view and the
1642    // camera are rebased onto the camera's own chunk.
1643    #[test]
1644    fn chunk_streaming_rebases_the_view_onto_the_camera_chunk() {
1645        let (_recorded, mut backend) = recording_backend();
1646        let mut state = empty_state();
1647        state.chunk_stream = Some(chunk_state(Arc::new(FailingChunk), 0, 0));
1648        // 16-unit chunks: floor(40/16) = 2, floor(-40/16) = -3.
1649        let cam = [40.0, 5.0, -40.0];
1650        let view = translation_view(-40.0, -5.0, 40.0);
1651        let (out_view, out_cam) = drive_once(&mut state, &mut backend, cam, view, false, None);
1652
1653        let origin = [32.0, 0.0, -48.0];
1654        assert_eq!(out_cam, [8.0, 5.0, 8.0]);
1655        assert_eq!(
1656            out_view,
1657            crate::gfx::chunk_coord::camera_relative_view(view, cam, origin)
1658        );
1659        assert_ne!(out_view, view, "the rebase actually rewrote the view");
1660        assert_eq!(
1661            state.chunk_stream.as_ref().unwrap().origin_chunk,
1662            ChunkCoord::new(2, -3)
1663        );
1664    }
1665
1666    // Crossing a chunk boundary moves the render origin and re-places every
1667    // resident chunk against it; staying put must not re-push identical models.
1668    #[test]
1669    fn crossing_into_a_new_chunk_rebases_resident_chunk_models() {
1670        let (recorded, mut backend) = recording_backend();
1671        let mut state = empty_state();
1672        let mut cs = chunk_state(Arc::new(FailingChunk), 1, 1);
1673        // Stand in for two chunks already uploaded at draw slots 3 and 4.
1674        cs.draws.insert(ChunkCoord::new(0, 0), 3);
1675        cs.draws.insert(ChunkCoord::new(1, 0), 4);
1676        state.chunk_stream = Some(cs);
1677
1678        drive_once(
1679            &mut state,
1680            &mut backend,
1681            [20.0, 0.0, 0.0],
1682            IDENTITY4,
1683            false,
1684            None,
1685        );
1686        {
1687            let s = recorded.lock().unwrap();
1688            assert!(s.saw(&Call::SetChunkModel(3)));
1689            assert!(s.saw(&Call::SetChunkModel(4)));
1690        }
1691        assert_eq!(
1692            state.chunk_stream.as_ref().unwrap().origin_chunk,
1693            ChunkCoord::new(1, 0)
1694        );
1695
1696        recorded.lock().unwrap().calls.clear();
1697        drive_once(
1698            &mut state,
1699            &mut backend,
1700            [21.0, 0.0, 0.0],
1701            IDENTITY4,
1702            false,
1703            None,
1704        );
1705        assert!(
1706            !recorded
1707                .lock()
1708                .unwrap()
1709                .calls
1710                .iter()
1711                .any(|c| matches!(c, Call::SetChunkModel(_)))
1712        );
1713    }
1714
1715    // A chunk that leaves the view window releases its draw slot on the
1716    // backend and stops being tracked as resident.
1717    #[test]
1718    fn chunks_leaving_the_view_window_are_removed_from_the_backend() {
1719        let (recorded, mut backend) = recording_backend();
1720        let mut state = empty_state();
1721        let mut cs = chunk_state(Arc::new(FailingChunk), 0, 0);
1722        // Stand in for chunk (0, 0) already uploaded at draw slot 9.
1723        cs.draws.insert(ChunkCoord::new(0, 0), 9);
1724        state.chunk_stream = Some(cs);
1725
1726        // Frame 1 at the origin puts (0, 0) in the window.
1727        drive_once(&mut state, &mut backend, [0.0; 3], IDENTITY4, false, None);
1728        // Frame 2 far east: (0, 0) is past the evict band.
1729        drive_once(
1730            &mut state,
1731            &mut backend,
1732            [800.0, 0.0, 0.0],
1733            IDENTITY4,
1734            false,
1735            None,
1736        );
1737
1738        assert!(recorded.lock().unwrap().saw(&Call::RemoveChunkMesh(9)));
1739        assert!(
1740            !state
1741                .chunk_stream
1742                .as_ref()
1743                .unwrap()
1744                .draws
1745                .contains_key(&ChunkCoord::new(0, 0))
1746        );
1747    }
1748
1749    // A generated chunk is added to the backend and its returned draw slot
1750    // recorded, so a later rebase or eviction can find it.
1751    #[test]
1752    fn a_generated_chunk_is_added_and_its_draw_slot_tracked() {
1753        let (recorded, mut backend) = recording_backend();
1754        let mut state = empty_state();
1755        state.chunk_stream = Some(chunk_state(Arc::new(ConstChunk), 0, 0));
1756        drive_until(&mut state, &mut backend, [0.0; 3], |s| {
1757            s.chunk_stream.as_ref().unwrap().streamer.stats().0 == 1
1758        });
1759
1760        let cs = state.chunk_stream.as_ref().unwrap();
1761        assert_eq!(cs.draws.get(&ChunkCoord::new(0, 0)), Some(&0));
1762        assert!(recorded.lock().unwrap().saw(&Call::AddChunkMesh));
1763    }
1764
1765    #[test]
1766    fn a_step_without_parked_state_publishes_no_view() {
1767        let mut w = StepWorld::new();
1768        assert_eq!(w.step(), StepResult::Continue);
1769        assert!(w.resources.get::<CameraRelativeView>().is_none());
1770    }
1771
1772    // Init succeeded but the backend is gone: the draw still needs a view, so
1773    // the absolute camera is published and the state stays parked.
1774    #[test]
1775    fn a_step_without_a_backend_still_publishes_the_absolute_view() {
1776        let view = translation_view(-1.0, -2.0, -3.0);
1777        let mut w = StepWorld::new().with_camera([1.0, 2.0, 3.0], view);
1778        w.resources.insert(empty_state());
1779
1780        assert_eq!(w.step(), StepResult::Continue);
1781        assert_eq!(w.view().cam_pos, [1.0, 2.0, 3.0]);
1782        assert_eq!(w.view().view, view);
1783        assert_eq!(w.parked_state().frame_count, 0, "no frame was driven");
1784    }
1785
1786    #[test]
1787    fn a_step_without_a_camera_publishes_the_identity_view() {
1788        let mut w = StepWorld::new();
1789        w.resources.insert(empty_state());
1790        w.park_render_queues();
1791
1792        w.step();
1793        assert_eq!(w.view().view, IDENTITY4);
1794        assert_eq!(w.view().cam_pos, [0.0; 3]);
1795    }
1796
1797    // The op queue + slot allocator are taken for the step and parked again
1798    // for the systems that follow, and the frame clock ticks once per step.
1799    #[test]
1800    fn a_step_drives_the_pools_and_reparks_the_queues() {
1801        let mut w = StepWorld::new().with_camera([0.0; 3], IDENTITY4);
1802        w.resources.insert(pooled_state(8));
1803        w.park_render_queues();
1804
1805        w.step();
1806        assert!(
1807            w.resources
1808                .get::<crate::ecs::ActiveRenderQueues>()
1809                .is_some_and(|slot| slot.0.is_some())
1810        );
1811        assert_eq!(w.parked_state().frame_count, 1);
1812        assert!(
1813            w.parked_state()
1814                .texture_streamer
1815                .as_ref()
1816                .unwrap()
1817                .stats()
1818                .2
1819                < 2,
1820            "the pools were driven"
1821        );
1822    }
1823
1824    // The overlay's flag is peeked, not taken: streaming pauses behind an
1825    // opaque menu, and GraphicsSystem still finds the frame later this tick.
1826    #[test]
1827    fn an_opaque_overlay_suspends_streaming_without_consuming_the_frame() {
1828        let mut w = StepWorld::new().with_camera([0.0; 3], IDENTITY4);
1829        w.resources.insert(pooled_state(8));
1830        w.park_render_queues();
1831        w.resources.insert(OverlayFrame {
1832            world_hidden: true,
1833            ..Default::default()
1834        });
1835
1836        w.step();
1837        assert_eq!(
1838            w.parked_state().texture_streamer.as_ref().unwrap().stats(),
1839            (0, 0, 2)
1840        );
1841        assert!(w.resources.get::<OverlayFrame>().is_some());
1842    }
1843
1844    // RSS is a syscall, so the valve only samples on its throttled cadence.
1845    #[test]
1846    fn ram_pressure_is_sampled_only_on_the_throttled_cadence() {
1847        let mut w = StepWorld::new();
1848        let mut state = empty_state();
1849        state.frame_count = 1;
1850        w.resources.insert(state);
1851        // A 1 MiB ceiling is under any real process RSS, so a sample that ran
1852        // would certainly engage the valve.
1853        w.resources
1854            .insert(crate::app::budget::MemoryBudget::compute(None, 1));
1855
1856        w.step();
1857        assert!(w.resources.get::<StreamingPressure>().is_none());
1858    }
1859
1860    // No published ceiling means no valve: streaming stays on its byte-budget
1861    // policy and nothing is reported.
1862    #[test]
1863    fn ram_pressure_is_not_sampled_without_a_memory_budget() {
1864        let mut w = StepWorld::new();
1865        w.resources.insert(empty_state());
1866        w.step();
1867        assert!(w.resources.get::<StreamingPressure>().is_none());
1868    }
1869
1870    #[test]
1871    fn an_rss_sample_over_the_memory_budget_publishes_engaged_pressure() {
1872        let mut w = StepWorld::new();
1873        w.resources.insert(empty_state());
1874        w.resources
1875            .insert(crate::app::budget::MemoryBudget::compute(None, 1));
1876
1877        w.step();
1878        // The valve is inert (and publishes nothing) where RSS cannot be read.
1879        match crate::app::sysmem::process_resident_bytes() {
1880            Some(_) => {
1881                let p = w.resources.get::<StreamingPressure>().expect("published");
1882                assert!(p.under_pressure);
1883                assert_eq!(p.budget_bytes, 1024 * 1024);
1884                assert_ne!(w.parked_state().pressure_stage, StreamPressureStage::None);
1885            }
1886            None => assert!(w.resources.get::<StreamingPressure>().is_none()),
1887        }
1888    }
1889}