1use 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
39const PRESSURE_SAMPLE_INTERVAL: u64 = 30;
43
44#[derive(Debug, Clone, Copy)]
52pub(crate) struct CameraRelativeView {
53 pub view: [[f32; 4]; 4],
54 pub cam_pos: [f32; 3],
55}
56
57pub(crate) struct ChunkStreamState {
62 pub(crate) streamer: crate::gfx::streaming::chunk::ChunkStreamer,
63 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 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#[derive(Debug, Clone, Default)]
82pub struct StreamingStats {
83 pub texture: Option<(usize, usize, usize)>,
85 pub mesh: Option<(usize, usize, usize)>,
87 pub chunk: Option<(usize, usize)>,
89 pub texture_bytes: Option<(u64, u64)>,
92 pub mesh_bytes: Option<(u64, u64)>,
94 pub chunk_bytes: Option<(u64, u64)>,
97}
98
99#[derive(Debug, Clone, Copy)]
106pub struct StreamingPressure {
107 pub rss_bytes: u64,
109 pub budget_bytes: u64,
111 pub under_pressure: bool,
113}
114
115pub(crate) struct StreamingState {
122 pub(crate) texture_streamer: Option<crate::gfx::streaming::texture::TextureStreamer>,
125 pub(crate) mesh_streamer: Option<crate::gfx::streaming::mesh::MeshStreamer>,
127 pub(crate) mesh_stream_draw_indices: Vec<usize>,
130 pub(crate) chunk_stream: Option<ChunkStreamState>,
133 pub(crate) shader_warmup: Option<crate::gfx::streaming::shader::ShaderWarmup>,
136 pub(crate) scene_residency: Option<SceneResidency>,
140 pub(crate) frame_count: u64,
142 pub(crate) frames_in_flight: usize,
146 pub(crate) texture_baseline_budget: Option<u64>,
151 pub(crate) mesh_baseline_budget: Option<u64>,
152 pub(crate) chunk_baseline_budget: Option<u64>,
153 pub(crate) pressure_stage: pressure::StreamPressureStage,
158 pub(crate) pressure_factor: f64,
159 pub(crate) last_sampled_rss: Option<u64>,
160 pub(crate) drift: crate::app::mem_drift::DriftTracker,
163 pub(crate) last_drift_verdict: Option<crate::app::mem_drift::DriftVerdict>,
166 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)]
184pub struct StreamingSystem {
186 scene_status_scratch: Vec<(AssetId, crate::gfx::scene_residency::SceneLoadState, f32)>,
189}
190
191impl StreamingSystem {
192 pub fn new() -> Self {
194 Self::default()
195 }
196}
197
198impl System for StreamingSystem {
199 fn step(&mut self, ctx: &mut PipelineContext) -> StepResult {
200 if !ctx.resources.contains::<StreamingState>() {
203 return StepResult::Continue;
204 }
205
206 let ram_budget = ctx
210 .resource::<crate::app::budget::MemoryBudget>()
211 .map(|b| b.budget_bytes);
212 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 let world_hidden = ctx
225 .resource::<OverlayFrame>()
226 .map(|o| o.world_hidden)
227 .unwrap_or(false);
228
229 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 {
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 let Some(mut queues) = crate::ecs::ActiveRenderQueues::take(ctx.resources) else {
279 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 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 accounting::publish(
313 concinnity_core::memory::ledger(),
314 state.pool_reports(transient_pool_bytes),
315 );
316 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 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 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(programs)) => {
365 ops.record(move |backend| {
369 let shader = crate::gfx::backend_init::WorldShader {
370 programs: Some(&programs),
371 deferred: false,
372 };
373 let started = std::time::Instant::now();
374 match backend.install_world_shader(bucket, shader) {
375 Ok(()) => tracing::info!(
378 "StreamingSystem: shader bucket {} pipeline ready ({:.1} ms)",
379 bucket,
380 started.elapsed().as_secs_f32() * 1000.0
381 ),
382 Err(e) => tracing::error!(
383 "StreamingSystem: shader bucket {} pipeline build failed: {}",
384 bucket,
385 e
386 ),
387 }
388 });
389 }
390 Some(Err(e)) => tracing::error!(
391 "StreamingSystem: shader bucket {} payload unreadable: {}",
392 bucket,
393 e
394 ),
395 None => {}
396 }
397 true
398 } else {
399 ops.record(move |backend| {
400 backend.evict_world_shader(bucket);
401 tracing::info!(
402 "StreamingSystem: shader bucket {} pipeline released",
403 bucket
404 );
405 });
406 false
407 };
408 if let Some(w) = self.shader_warmup.as_mut() {
409 w.note_resident(bucket, resident);
410 }
411 if let Some(residency) = self.scene_residency.as_mut() {
412 residency.note_resident((CHANNEL_SHADER, bucket), resident);
413 }
414 }
415
416 pub(crate) fn apply_op_failures(&mut self, failures: &[OpFailure], slots: &mut RenderSlots) {
421 for &failure in failures {
422 match failure {
423 OpFailure::MeshUpload { stream_id } => {
424 if let Some(streamer) = &mut self.mesh_streamer {
425 streamer.note_upload_failed(stream_id);
426 }
427 if let Some(residency) = &mut self.scene_residency {
428 residency.note_resident((CHANNEL_MESH, stream_id as u32), false);
429 }
430 }
431 OpFailure::ChunkAdd { coord } => {
432 if let Some(cs) = &mut self.chunk_stream
433 && let Some(draw_idx) = cs.draws.remove(&coord)
434 {
435 slots.free_draw(draw_idx);
436 tracing::warn!(
437 "StreamingSystem: chunk add ({},{}) rolled back",
438 coord.x,
439 coord.z
440 );
441 }
442 }
443 }
444 }
445 }
446
447 fn drive(
451 &mut self,
452 ops: &mut RenderOps,
453 slots: &mut RenderSlots,
454 cam_pos: [f32; 3],
455 view_matrix: [[f32; 4]; 4],
456 world_hidden: bool,
457 scene_pins: Option<&[AssetId]>,
458 ) -> ([[f32; 4]; 4], [f32; 3]) {
459 let loads_frozen = self.pressure_stage.freezes_loads();
463
464 if let (Some(residency), Some(pins)) = (self.scene_residency.as_mut(), scene_pins) {
468 let changes = residency.sync_pins(pins);
469 for (members, blocked) in [(&changes.blocked, true), (&changes.unblocked, false)] {
470 for &(channel, id) in members {
471 match channel {
472 CHANNEL_TEXTURE => {
473 if let Some(s) = &mut self.texture_streamer {
474 s.set_blocked(id as usize, blocked);
475 }
476 }
477 CHANNEL_MESH => {
478 if let Some(s) = &mut self.mesh_streamer {
479 s.set_blocked(id as usize, blocked);
480 }
481 }
482 CHANNEL_SHADER => {
483 if let Some(w) = &mut self.shader_warmup {
484 w.set_blocked(id, blocked);
485 }
486 }
487 _ => {}
488 }
489 }
490 }
491 }
492
493 self.drive_shader_warmup(ops);
498
499 let world_hidden = world_hidden
503 && !self
504 .scene_residency
505 .as_ref()
506 .is_some_and(|r| r.any_loading());
507
508 if !world_hidden && let Some(streamer) = &mut self.texture_streamer {
514 streamer.update_scores(cam_pos, self.frame_count);
515 if !loads_frozen {
516 for slot in streamer.plan_and_dispatch() {
517 ops.record(move |backend| {
518 if let Err(e) = backend.evict_texture_slot(slot) {
519 tracing::warn!("StreamingSystem: texture evict slot {}: {}", slot, e);
520 }
521 });
522 if let Some(residency) = self.scene_residency.as_mut() {
523 residency.note_resident((CHANNEL_TEXTURE, slot as u32), false);
524 }
525 }
526 }
527 let residency = &mut self.scene_residency;
528 streamer.drain_completed(self.frame_count, |slot, image| {
529 ops.record(move |backend| {
530 if let Err(e) = backend.update_texture_slot(slot, &image) {
531 tracing::warn!("StreamingSystem: texture upload slot {}: {}", slot, e);
532 }
533 });
534 if let Some(residency) = residency.as_mut() {
535 residency.note_resident((CHANNEL_TEXTURE, slot as u32), true);
536 }
537 });
538 if let Some((resident, pending, unloaded)) = self
541 .heartbeats
542 .texture
543 .sample(self.frame_count, || streamer.stats())
544 {
545 tracing::info!(
546 "StreamingSystem: texture streaming -- {} resident, {} pending, {} unloaded",
547 resident,
548 pending,
549 unloaded
550 );
551 }
552 }
553
554 if !world_hidden && let Some(streamer) = &mut self.mesh_streamer {
559 streamer.update_scores(cam_pos, self.frame_count);
560 if !loads_frozen {
561 let retire_frame = self.frame_count + self.frames_in_flight as u64;
564 for stream_id in streamer.plan_and_dispatch() {
565 if let Some(&draw_idx) = self.mesh_stream_draw_indices.get(stream_id) {
566 ops.record(move |backend| {
567 if let Err(e) = backend.evict_mesh(draw_idx, retire_frame) {
568 tracing::warn!(
569 "StreamingSystem: mesh evict draw {}: {}",
570 draw_idx,
571 e
572 );
573 }
574 });
575 }
576 if let Some(residency) = self.scene_residency.as_mut() {
577 residency.note_resident((CHANNEL_MESH, stream_id as u32), false);
578 }
579 }
580 }
581 let draw_indices = &self.mesh_stream_draw_indices;
582 let frame = self.frame_count;
583 let residency = &mut self.scene_residency;
584 streamer.drain_completed(self.frame_count, |stream_id, verts, idxs| {
585 if let Some(&draw_idx) = draw_indices.get(stream_id) {
589 ops.record_with(move |backend, out| {
590 if let Err(e) = backend.upload_mesh(draw_idx, &verts, &idxs, frame) {
591 tracing::debug!(
592 "StreamingSystem: mesh upload draw {} deferred: {}",
593 draw_idx,
594 e
595 );
596 out.memory_pressure |=
597 matches!(e, crate::gfx::error::RenderError::OutOfDeviceMemory(_));
598 out.failures.push(OpFailure::MeshUpload { stream_id });
599 }
600 });
601 }
602 if let Some(residency) = residency.as_mut() {
603 residency.note_resident((CHANNEL_MESH, stream_id as u32), true);
604 }
605 });
606 if let Some((resident, pending, unloaded)) = self
607 .heartbeats
608 .mesh
609 .sample(self.frame_count, || streamer.stats())
610 {
611 tracing::info!(
612 "StreamingSystem: mesh streaming -- {} resident, {} pending, {} unloaded",
613 resident,
614 pending,
615 unloaded
616 );
617 }
618 }
619
620 let mut final_view = view_matrix;
632 let mut final_cam_pos = cam_pos;
633 if let Some(cs) = &mut self.chunk_stream {
634 let camera_chunk = cs.streamer.camera_chunk(cam_pos);
635 let retire_frame = self.frame_count + self.frames_in_flight as u64;
636 if !loads_frozen {
642 for coord in cs.streamer.plan_and_dispatch(camera_chunk) {
643 if let Some(draw_idx) = cs.draws.remove(&coord) {
644 slots.free_draw(draw_idx);
645 ops.record(move |backend| {
646 if let Err(e) = backend.remove_chunk_mesh(draw_idx, retire_frame) {
647 tracing::warn!(
648 "StreamingSystem: chunk remove ({},{}): {}",
649 coord.x,
650 coord.z,
651 e
652 );
653 }
654 });
655 }
656 }
657 }
658 if camera_chunk != cs.origin_chunk {
663 for (&coord, &draw_idx) in &cs.draws {
664 let model = chunk_model_matrix(coord, camera_chunk, cs.chunk_w, cs.chunk_d);
665 ops.record(move |backend| {
666 if let Err(e) = backend.set_chunk_model(draw_idx, model) {
667 tracing::warn!(
668 "StreamingSystem: chunk rebase ({},{}): {}",
669 coord.x,
670 coord.z,
671 e
672 );
673 }
674 });
675 }
676 cs.origin_chunk = camera_chunk;
677 }
678 let frame = self.frame_count;
679 let (chunk_w, chunk_d) = (cs.chunk_w, cs.chunk_d);
680 let (tex, nm, mat) = (cs.texture_slot, cs.normal_map_slot, cs.material);
681 let mut added: Vec<(crate::gfx::chunk_coord::ChunkCoord, usize)> = Vec::new();
682 cs.streamer.drain_completed(|coord, verts, idxs| {
683 if verts.is_empty() || idxs.is_empty() {
684 tracing::warn!(
685 "StreamingSystem: chunk add ({},{}): empty chunk geometry",
686 coord.x,
687 coord.z
688 );
689 return;
690 }
691 let model = chunk_model_matrix(coord, camera_chunk, chunk_w, chunk_d);
692 let dst = slots.allocate_draw();
693 let draw_idx = match dst {
694 crate::gfx::draw_slot::SlotAlloc::Reuse(i)
695 | crate::gfx::draw_slot::SlotAlloc::Append(i) => i,
696 };
697 added.push((coord, draw_idx));
698 ops.record_with(move |backend, out| {
701 let mesh = ChunkMesh {
702 verts: &verts,
703 idxs: &idxs,
704 model,
705 texture_slot: tex,
706 normal_map_slot: nm,
707 material: mat,
708 frame,
709 };
710 if let Err(e) = backend.add_chunk_mesh(mesh, dst) {
711 tracing::warn!(
712 "StreamingSystem: chunk add ({},{}): {}",
713 coord.x,
714 coord.z,
715 e
716 );
717 out.memory_pressure |=
718 matches!(e, crate::gfx::error::RenderError::OutOfDeviceMemory(_));
719 out.failures.push(OpFailure::ChunkAdd { coord });
720 }
721 });
722 });
723 for (coord, draw_idx) in added {
724 cs.draws.insert(coord, draw_idx);
725 }
726 let (ox, oz) = camera_chunk.origin_world(cs.chunk_w, cs.chunk_d);
729 let origin = [ox, 0.0, oz];
730 final_view =
731 crate::gfx::chunk_coord::camera_relative_view(view_matrix, cam_pos, origin);
732 final_cam_pos = [cam_pos[0] - ox, cam_pos[1], cam_pos[2] - oz];
733 if let Some((resident, pending, near, far)) =
734 self.heartbeats.chunk.sample(self.frame_count, || {
735 let (resident, pending) = cs.streamer.stats();
736 let (near, far) = cs.streamer.detail_counts();
737 (resident, pending, near, far)
738 })
739 {
740 tracing::info!(
741 "StreamingSystem: chunk streaming -- {} resident ({} full, {} impostor), {} pending",
742 resident,
743 near,
744 far,
745 pending
746 );
747 }
748 }
749
750 self.frame_count += 1;
751 (final_view, final_cam_pos)
752 }
753
754 fn sample_pressure(&mut self, rss: Option<u64>, budget: u64) -> Option<StreamingPressure> {
759 let rss = rss?;
760 let rising = self.last_sampled_rss.is_some_and(|prev| rss > prev);
761 let prev_stage = self.pressure_stage;
762 let decision =
763 pressure::step_pressure(rss, budget, rising, prev_stage, self.pressure_factor);
764
765 use pressure::StreamPressureStage::Evict;
771 match (prev_stage, decision.stage) {
772 (_, Evict) => self.apply_byte_factor(decision.budget_factor),
773 (Evict, _) => self.apply_byte_factor(1.0),
774 _ => {}
775 }
776
777 self.pressure_stage = decision.stage;
778 self.pressure_factor = decision.budget_factor;
779 self.last_sampled_rss = Some(rss);
780 Some(StreamingPressure {
781 rss_bytes: rss,
782 budget_bytes: budget,
783 under_pressure: decision.stage != pressure::StreamPressureStage::None,
784 })
785 }
786
787 fn sample_drift(
791 &mut self,
792 rss: Option<u64>,
793 budget: u64,
794 ) -> Option<crate::app::mem_drift::MemoryDrift> {
795 use crate::app::mem_drift::DriftVerdict;
796 let rss = rss?;
797 let heap_live = concinnity_core::memory::stats()?.live_bytes;
798 let drift = self.drift.sample(rss, heap_live, budget)?;
799
800 if self.last_drift_verdict != Some(drift.verdict) {
801 self.last_drift_verdict = Some(drift.verdict);
802 let heap_mib = drift.heap_growth_bytes / (1024 * 1024);
803 let outside_mib = drift.outside_heap_growth_bytes / (1024 * 1024);
804 let minutes = drift.window_secs / 60;
805 let reading = drift.verdict.label();
806 if drift.verdict == DriftVerdict::Settled {
807 tracing::info!(
808 "memory drift: {reading} -- heap {heap_mib:+} MiB, outside heap {outside_mib:+} MiB over {minutes} min"
809 );
810 } else {
811 tracing::warn!(
812 "memory drift: {reading} -- heap {heap_mib:+} MiB, outside heap {outside_mib:+} MiB over {minutes} min"
813 );
814 }
815 }
816 Some(drift)
817 }
818
819 fn apply_byte_factor(&mut self, factor: f64) {
822 if let (Some(streamer), Some(baseline)) =
823 (self.texture_streamer.as_mut(), self.texture_baseline_budget)
824 {
825 streamer.set_byte_budget(Some(pressure::scale_budget(baseline, factor)));
826 }
827 if let (Some(streamer), Some(baseline)) =
828 (self.mesh_streamer.as_mut(), self.mesh_baseline_budget)
829 {
830 streamer.set_byte_budget(Some(pressure::scale_budget(baseline, factor)));
831 }
832 if let (Some(cs), Some(baseline)) = (self.chunk_stream.as_mut(), self.chunk_baseline_budget)
835 {
836 cs.streamer
837 .set_byte_budget(Some(pressure::scale_budget(baseline, factor)));
838 }
839 }
840
841 fn pool_reports(
852 &self,
853 transient_pool_bytes: u64,
854 ) -> impl Iterator<Item = accounting::PoolReport> {
855 let textures = accounting::textures_report(
856 self.texture_streamer
857 .as_ref()
858 .map(|s| (s.resident_bytes(), s.byte_budget())),
859 transient_pool_bytes,
860 );
861 [
862 self.mesh_streamer.as_ref().map(|s| {
863 (
864 concinnity_core::memory::MemTag::Meshes,
865 s.resident_bytes(),
866 s.byte_budget(),
867 )
868 }),
869 self.chunk_stream.as_ref().map(|cs| {
870 (
871 concinnity_core::memory::MemTag::Chunks,
872 cs.streamer.resident_bytes(),
873 cs.streamer.byte_budget(),
874 )
875 }),
876 ]
877 .into_iter()
878 .flatten()
879 .map(
880 |(tag, resident_bytes, byte_budget)| accounting::PoolReport {
881 tag,
882 resident_bytes,
883 byte_budget,
884 },
885 )
886 .chain(textures)
887 }
888
889 pub(crate) fn streaming_stats(&self) -> StreamingStats {
893 StreamingStats {
894 texture: self.texture_streamer.as_ref().map(|s| s.stats()),
895 mesh: self.mesh_streamer.as_ref().map(|s| s.stats()),
896 chunk: self.chunk_stream.as_ref().map(|cs| cs.streamer.stats()),
897 texture_bytes: self
898 .texture_streamer
899 .as_ref()
900 .map(|s| (s.resident_bytes(), s.byte_budget().unwrap_or(0))),
901 mesh_bytes: self
902 .mesh_streamer
903 .as_ref()
904 .map(|s| (s.resident_bytes(), s.byte_budget().unwrap_or(0))),
905 chunk_bytes: self.chunk_stream.as_ref().map(|cs| {
906 (
907 cs.streamer.resident_bytes(),
908 cs.streamer.byte_budget().unwrap_or(0),
909 )
910 }),
911 }
912 }
913}
914
915pub(crate) fn chunk_model_matrix(
921 coord: crate::gfx::chunk_coord::ChunkCoord,
922 origin: crate::gfx::chunk_coord::ChunkCoord,
923 chunk_w: f32,
924 chunk_d: f32,
925) -> [[f32; 4]; 4] {
926 let dx = (coord.x - origin.x) as f32 * chunk_w;
927 let dz = (coord.z - origin.z) as f32 * chunk_d;
928 [
929 [1.0, 0.0, 0.0, 0.0],
930 [0.0, 1.0, 0.0, 0.0],
931 [0.0, 0.0, 1.0, 0.0],
932 [dx, 0.0, dz, 1.0],
933 ]
934}
935
936#[cfg(test)]
937mod tests {
938 use super::*;
939 use crate::blob::BlobData;
940 use crate::ecs::{ComponentStorage, Resources};
941 use crate::gfx::chunk_coord::ChunkCoord;
942 use crate::gfx::chunk_window::ChunkDetail;
943 use crate::gfx::mesh_payload::Vertex;
944 use crate::gfx::mock_backend::{Call, MockBackend, recording_backend};
945 use crate::gfx::profile::FrameProfile;
946 use crate::gfx::streaming::chunk::{ChunkSource, ChunkStreamer};
947 use crate::gfx::streaming::mesh::{DecodedMesh, MeshPayloadSource, MeshStreamer};
948 use crate::gfx::streaming::texture::{DecodedTexture, PayloadSource, TextureStreamer};
949 use pressure::StreamPressureStage;
950 use std::sync::Arc;
951
952 const MAX_DRIVE_SPINS: usize = 100_000;
956
957 fn vtx() -> Vertex {
958 Vertex {
959 pos: [0.0; 3],
960 normal: [0.0, 1.0, 0.0],
961 tangent: [1.0, 0.0, 0.0],
962 color: [1.0; 3],
963 uv: [0.0; 2],
964 }
965 }
966
967 fn tri() -> DecodedMesh {
968 DecodedMesh {
969 vertices: vec![vtx(), vtx(), vtx()],
970 indices: vec![0, 1, 2],
971 }
972 }
973
974 struct ConstTexture;
977 impl PayloadSource for ConstTexture {
978 fn fetch(&self, _id: usize) -> Result<DecodedTexture, String> {
979 Ok(DecodedTexture {
980 image: crate::bake::texture::TextureImage::rgba8(1, 1, vec![1, 2, 3, 4]),
981 })
982 }
983 }
984
985 struct ConstMesh;
986 impl MeshPayloadSource for ConstMesh {
987 fn fetch(&self, _id: usize) -> Result<DecodedMesh, String> {
988 Ok(tri())
989 }
990 }
991
992 struct ConstChunk;
993 impl ChunkSource for ConstChunk {
994 fn generate(
995 &self,
996 _coord: ChunkCoord,
997 _detail: ChunkDetail,
998 ) -> Result<DecodedMesh, String> {
999 Ok(tri())
1000 }
1001 }
1002
1003 struct FailingChunk;
1007 impl ChunkSource for FailingChunk {
1008 fn generate(
1009 &self,
1010 _coord: ChunkCoord,
1011 _detail: ChunkDetail,
1012 ) -> Result<DecodedMesh, String> {
1013 Err("test source".to_string())
1014 }
1015 }
1016
1017 fn translation_view(x: f32, y: f32, z: f32) -> [[f32; 4]; 4] {
1020 [
1021 [1.0, 0.0, 0.0, 0.0],
1022 [0.0, 1.0, 0.0, 0.0],
1023 [0.0, 0.0, 1.0, 0.0],
1024 [x, y, z, 1.0],
1025 ]
1026 }
1027
1028 fn empty_state() -> StreamingState {
1033 StreamingState {
1034 texture_streamer: None,
1035 mesh_streamer: None,
1036 mesh_stream_draw_indices: Vec::new(),
1037 chunk_stream: None,
1038 shader_warmup: None,
1039 scene_residency: None,
1040 frame_count: 0,
1041 frames_in_flight: 2,
1042 texture_baseline_budget: None,
1043 mesh_baseline_budget: None,
1044 chunk_baseline_budget: None,
1045 pressure_stage: StreamPressureStage::None,
1046 pressure_factor: 1.0,
1047 last_sampled_rss: None,
1048 drift: Default::default(),
1049 last_drift_verdict: None,
1050 heartbeats: Default::default(),
1051 }
1052 }
1053
1054 fn pooled_state(resident_cap: usize) -> StreamingState {
1060 let centers = vec![vec![[0.0, 0.0, 0.0]], vec![[100.0, 0.0, 0.0]]];
1061 let mut state = empty_state();
1062 state.texture_streamer = Some(TextureStreamer::new(
1063 Arc::new(ConstTexture),
1064 centers.clone(),
1065 4,
1066 resident_cap,
1067 ));
1068 state.mesh_streamer = Some(MeshStreamer::new(
1069 Arc::new(ConstMesh),
1070 centers,
1071 4,
1072 resident_cap,
1073 ));
1074 state.mesh_stream_draw_indices = vec![10, 11];
1075 state
1076 }
1077
1078 fn chunk_state(source: Arc<dyn ChunkSource>, near: i32, far: i32) -> ChunkStreamState {
1080 ChunkStreamState {
1081 streamer: ChunkStreamer::new(source, near, far, 64, 16.0, 16.0),
1082 draws: std::collections::BTreeMap::new(),
1083 chunk_w: 16.0,
1084 chunk_d: 16.0,
1085 origin_chunk: ChunkCoord::new(0, 0),
1086 texture_slot: 0,
1087 normal_map_slot: crate::gfx::render_types::NO_NORMAL_MAP_SLOT,
1088 material: crate::gfx::render_types::MaterialUniforms::DEFAULT,
1089 }
1090 }
1091
1092 fn drive_once(
1099 state: &mut StreamingState,
1100 backend: &mut MockBackend,
1101 cam: [f32; 3],
1102 view: [[f32; 4]; 4],
1103 world_hidden: bool,
1104 pins: Option<&[AssetId]>,
1105 ) -> ([[f32; 4]; 4], [f32; 3]) {
1106 let mut slots = RenderSlots::new(0, true, &[]);
1107 let mut ops = RenderOps::default();
1108 let out = state.drive(&mut ops, &mut slots, cam, view, world_hidden, pins);
1109 let outcome = ops.replay(backend);
1110 state.apply_op_failures(&outcome.failures, &mut slots);
1111 out
1112 }
1113
1114 fn drive_until(
1115 state: &mut StreamingState,
1116 backend: &mut MockBackend,
1117 cam: [f32; 3],
1118 done: impl Fn(&StreamingState) -> bool,
1119 ) {
1120 let mut slots = RenderSlots::new(0, true, &[]);
1121 for _ in 0..MAX_DRIVE_SPINS {
1122 let mut ops = RenderOps::default();
1123 state.drive(&mut ops, &mut slots, cam, IDENTITY4, false, None);
1124 let outcome = ops.replay(backend);
1125 state.apply_op_failures(&outcome.failures, &mut slots);
1126 if done(state) {
1127 return;
1128 }
1129 std::thread::yield_now();
1130 }
1131 panic!("streaming never reached the expected state");
1132 }
1133
1134 struct StepWorld {
1136 components: ComponentStorage,
1137 blob: BlobData,
1138 profile: FrameProfile,
1139 resources: Resources,
1140 scratch: crate::ecs::Arena,
1141 }
1142
1143 impl StepWorld {
1144 fn new() -> Self {
1145 Self {
1146 components: ComponentStorage::default(),
1147 blob: BlobData::empty(),
1148 profile: FrameProfile::default(),
1149 resources: Resources::new(),
1150 scratch: crate::ecs::Arena::with_capacity(64 * 1024),
1151 }
1152 }
1153
1154 fn with_camera(mut self, position: [f32; 3], view_matrix: [[f32; 4]; 4]) -> Self {
1156 self.components.push_typed(Camera3D {
1157 position,
1158 view_matrix,
1159 ..Camera3D::bake(Default::default())
1160 });
1161 self
1162 }
1163
1164 fn park_render_queues(&mut self) {
1167 self.resources.insert(crate::ecs::ActiveRenderQueues(Some(
1168 crate::ecs::RenderQueues {
1169 ops: Default::default(),
1170 slots: RenderSlots::new(0, true, &[]),
1171 },
1172 )));
1173 }
1174
1175 fn step(&mut self) -> StepResult {
1176 let mut ctx = PipelineContext {
1177 components: &mut self.components,
1178 blob: &mut self.blob,
1179 profile: &mut self.profile,
1180 resources: &mut self.resources,
1181 frame: crate::ecs::FrameContext::new(&self.scratch),
1182 };
1183 StreamingSystem::new().step(&mut ctx)
1184 }
1185
1186 fn view(&self) -> CameraRelativeView {
1187 *self
1188 .resources
1189 .get::<CameraRelativeView>()
1190 .expect("camera-relative view published")
1191 }
1192
1193 fn parked_state(&self) -> &StreamingState {
1194 self.resources
1195 .get::<StreamingState>()
1196 .expect("state parked again")
1197 }
1198 }
1199
1200 #[test]
1201 fn sample_pressure_engages_and_publishes() {
1202 let mut s = empty_state();
1203 let p = s.sample_pressure(Some(920), 1000).expect("published");
1205 assert_eq!(s.pressure_stage, StreamPressureStage::Gate);
1206 assert!(p.under_pressure);
1207 assert_eq!(p.rss_bytes, 920);
1208 assert_eq!(p.budget_bytes, 1000);
1209 }
1210
1211 #[test]
1215 fn sample_drift_reports_once_settled_and_splits_the_whole_rss_movement() {
1216 const RSS: u64 = 2 * 1024 * 1024 * 1024;
1217 const BUDGET: u64 = 4 * RSS;
1218 let mut s = empty_state();
1219
1220 assert_eq!(s.sample_drift(None, BUDGET), None, "no RSS, no drift");
1221 let d = (0..16)
1224 .find_map(|_| s.sample_drift(Some(RSS), BUDGET))
1225 .expect("a steady run settles the baseline");
1226 assert_eq!(d.verdict, crate::app::mem_drift::DriftVerdict::Settled);
1227 assert_eq!(d.heap_growth_bytes + d.outside_heap_growth_bytes, 0);
1230 }
1231
1232 #[test]
1235 fn sample_drift_never_moves_the_valve() {
1236 const RSS: u64 = 2 * 1024 * 1024 * 1024;
1237 let mut s = empty_state();
1238 for _ in 0..4 {
1239 s.sample_drift(Some(RSS), 4 * RSS);
1240 }
1241 assert_eq!(s.pressure_stage, StreamPressureStage::None);
1242 assert_eq!(s.pressure_factor, 1.0);
1243 assert_eq!(s.last_sampled_rss, None);
1244 }
1245
1246 #[test]
1247 fn sample_pressure_escalates_when_rss_keeps_rising() {
1248 let mut s = empty_state();
1249 s.sample_pressure(Some(910), 1000);
1250 assert_eq!(s.pressure_stage, StreamPressureStage::Gate);
1251 s.sample_pressure(Some(925), 1000);
1253 assert_eq!(s.pressure_stage, StreamPressureStage::Evict);
1254 assert!(s.pressure_factor < 1.0);
1255 }
1256
1257 #[test]
1258 fn sample_pressure_releases_with_hysteresis() {
1259 let mut s = empty_state();
1260 s.sample_pressure(Some(970), 1000); assert_eq!(s.pressure_stage, StreamPressureStage::Evict);
1262 s.sample_pressure(Some(850), 1000);
1264 assert_eq!(s.pressure_stage, StreamPressureStage::Evict);
1265 let p = s.sample_pressure(Some(700), 1000).expect("published");
1267 assert_eq!(s.pressure_stage, StreamPressureStage::None);
1268 assert_eq!(s.pressure_factor, 1.0);
1269 assert!(!p.under_pressure);
1270 }
1271
1272 #[test]
1273 fn sample_pressure_is_inert_without_rss() {
1274 let mut s = empty_state();
1275 s.sample_pressure(Some(970), 1000);
1276 let stage_before = s.pressure_stage;
1277 assert!(s.sample_pressure(None, 1000).is_none());
1279 assert_eq!(s.pressure_stage, stage_before);
1280 }
1281
1282 #[test]
1285 fn chunk_model_matrix_offsets_by_chunk_delta() {
1286 let m = chunk_model_matrix(ChunkCoord::new(2, -3), ChunkCoord::new(0, 0), 16.0, 10.0);
1287 assert_eq!(m[3], [32.0, 0.0, -30.0, 1.0]);
1288 assert_eq!(m[0], [1.0, 0.0, 0.0, 0.0]);
1289 assert_eq!(m[1], [0.0, 1.0, 0.0, 0.0]);
1290 assert_eq!(m[2], [0.0, 0.0, 1.0, 0.0]);
1291 }
1292
1293 #[test]
1294 fn chunk_model_matrix_origin_chunk_is_untranslated() {
1295 let c = ChunkCoord::new(5, 7);
1296 let m = chunk_model_matrix(c, c, 16.0, 16.0);
1297 assert_eq!(m[3], [0.0, 0.0, 0.0, 1.0]);
1298 }
1299
1300 #[test]
1303 fn deep_pressure_scales_each_pool_budget_and_release_restores_the_baseline() {
1304 let mut state = pooled_state(8);
1305 state.chunk_stream = Some(chunk_state(Arc::new(ConstChunk), 0, 0));
1306 state.texture_baseline_budget = Some(4000);
1307 state.mesh_baseline_budget = Some(2000);
1308 state.chunk_baseline_budget = Some(1000);
1309 state.apply_byte_factor(1.0);
1310
1311 state.sample_pressure(Some(970), 1000);
1312 assert_eq!(state.pressure_stage, StreamPressureStage::Evict);
1313 let factor = state.pressure_factor;
1314 assert!(factor < 1.0);
1315 let tex = state.texture_streamer.as_ref().unwrap().byte_budget();
1316 let mesh = state.mesh_streamer.as_ref().unwrap().byte_budget();
1317 let chunk = state.chunk_stream.as_ref().unwrap().streamer.byte_budget();
1318 assert_eq!(tex, Some(pressure::scale_budget(4000, factor)));
1319 assert_eq!(mesh, Some(pressure::scale_budget(2000, factor)));
1320 assert_eq!(chunk, Some(pressure::scale_budget(1000, factor)));
1321
1322 state.sample_pressure(Some(100), 1000);
1323 assert_eq!(state.pressure_stage, StreamPressureStage::None);
1324 assert_eq!(
1325 state.texture_streamer.as_ref().unwrap().byte_budget(),
1326 Some(4000)
1327 );
1328 assert_eq!(
1329 state.mesh_streamer.as_ref().unwrap().byte_budget(),
1330 Some(2000)
1331 );
1332 assert_eq!(
1333 state.chunk_stream.as_ref().unwrap().streamer.byte_budget(),
1334 Some(1000)
1335 );
1336 }
1337
1338 #[test]
1341 fn count_only_pools_gain_no_byte_budget_under_pressure() {
1342 let mut state = pooled_state(8);
1343 state.chunk_stream = Some(chunk_state(Arc::new(ConstChunk), 0, 0));
1344
1345 state.sample_pressure(Some(970), 1000);
1346 assert_eq!(state.pressure_stage, StreamPressureStage::Evict);
1347 assert_eq!(state.texture_streamer.as_ref().unwrap().byte_budget(), None);
1348 assert_eq!(state.mesh_streamer.as_ref().unwrap().byte_budget(), None);
1349 assert_eq!(
1350 state.chunk_stream.as_ref().unwrap().streamer.byte_budget(),
1351 None
1352 );
1353 }
1354
1355 #[test]
1356 fn streaming_stats_reports_every_active_pool() {
1357 let mut state = pooled_state(8);
1358 state.chunk_stream = Some(chunk_state(Arc::new(ConstChunk), 0, 0));
1359 state
1360 .texture_streamer
1361 .as_mut()
1362 .unwrap()
1363 .set_byte_budget(Some(4000));
1364
1365 let stats = state.streaming_stats();
1366 assert_eq!(stats.texture, Some((0, 0, 2)));
1367 assert_eq!(stats.mesh, Some((0, 0, 2)));
1368 assert_eq!(stats.chunk, Some((0, 0)));
1369 assert_eq!(stats.texture_bytes, Some((0, 4000)));
1370 assert_eq!(stats.mesh_bytes, Some((0, 0)));
1372 assert_eq!(stats.chunk_bytes, Some((0, 0)));
1373 }
1374
1375 #[test]
1376 fn streaming_stats_reports_nothing_without_pools() {
1377 let stats = empty_state().streaming_stats();
1378 assert!(stats.texture.is_none());
1379 assert!(stats.mesh.is_none());
1380 assert!(stats.chunk.is_none());
1381 assert!(stats.texture_bytes.is_none());
1382 assert!(stats.mesh_bytes.is_none());
1383 assert!(stats.chunk_bytes.is_none());
1384 }
1385
1386 #[test]
1389 fn debug_reports_the_active_pools_and_the_frame_clock() {
1390 let mut state = pooled_state(8);
1391 state.frame_count = 7;
1392 state.pressure_stage = StreamPressureStage::Gate;
1393
1394 let s = format!("{state:?}");
1395 assert!(s.contains("frame_count: 7"), "{s}");
1396 assert!(s.contains("texture: true"), "{s}");
1397 assert!(s.contains("mesh: true"), "{s}");
1398 assert!(s.contains("chunk: false"), "{s}");
1399 assert!(s.contains("pressure: Gate"), "{s}");
1400 }
1401
1402 #[test]
1403 fn drive_advances_the_frame_clock() {
1404 let (_recorded, mut backend) = recording_backend();
1405 let mut state = empty_state();
1406 drive_once(&mut state, &mut backend, [0.0; 3], IDENTITY4, false, None);
1407 drive_once(&mut state, &mut backend, [0.0; 3], IDENTITY4, false, None);
1408 assert_eq!(state.frame_count, 2);
1409 }
1410
1411 #[test]
1414 fn a_hidden_world_dispatches_no_loads() {
1415 let (_recorded, mut backend) = recording_backend();
1416 let mut state = pooled_state(8);
1417 drive_once(&mut state, &mut backend, [0.0; 3], IDENTITY4, true, None);
1418 assert_eq!(state.texture_streamer.as_ref().unwrap().stats(), (0, 0, 2));
1419 assert_eq!(state.mesh_streamer.as_ref().unwrap().stats(), (0, 0, 2));
1420 }
1421
1422 #[test]
1423 fn a_visible_world_dispatches_texture_and_mesh_loads() {
1424 let (_recorded, mut backend) = recording_backend();
1425 let mut state = pooled_state(8);
1426 drive_once(&mut state, &mut backend, [0.0; 3], IDENTITY4, false, None);
1427 assert!(state.texture_streamer.as_ref().unwrap().stats().2 < 2);
1429 assert!(state.mesh_streamer.as_ref().unwrap().stats().2 < 2);
1430 }
1431
1432 #[test]
1435 fn scene_residency_streams_only_the_pinned_scene_and_swaps_on_switch() {
1436 use crate::gfx::scene_residency::SceneLoadState;
1437
1438 let (_recorded, mut backend) = recording_backend();
1439 let mut state = pooled_state(8);
1440 let scene_a = AssetId(70);
1441 let scene_b = AssetId(71);
1442 let residency = SceneResidency::new(vec![
1443 (scene_a, vec![(CHANNEL_TEXTURE, 0), (CHANNEL_MESH, 0)]),
1444 (scene_b, vec![(CHANNEL_TEXTURE, 1), (CHANNEL_MESH, 1)]),
1445 ]);
1446 for (channel, id) in residency.all_members().collect::<Vec<_>>() {
1448 match channel {
1449 CHANNEL_TEXTURE => state
1450 .texture_streamer
1451 .as_mut()
1452 .unwrap()
1453 .set_blocked(id as usize, true),
1454 _ => state
1455 .mesh_streamer
1456 .as_mut()
1457 .unwrap()
1458 .set_blocked(id as usize, true),
1459 }
1460 }
1461 state.scene_residency = Some(residency);
1462
1463 let pins_a = [scene_a];
1465 for _ in 0..MAX_DRIVE_SPINS {
1466 drive_once(
1467 &mut state,
1468 &mut backend,
1469 [0.0; 3],
1470 IDENTITY4,
1471 false,
1472 Some(&pins_a),
1473 );
1474 let r = state.scene_residency.as_ref().unwrap();
1475 if r.state(scene_a) == Some(SceneLoadState::Resident) {
1476 break;
1477 }
1478 std::thread::yield_now();
1479 }
1480 let r = state.scene_residency.as_ref().unwrap();
1481 assert_eq!(r.state(scene_a), Some(SceneLoadState::Resident));
1482 assert_eq!(r.state(scene_b), Some(SceneLoadState::Unloaded));
1483 assert_eq!(
1484 state.texture_streamer.as_ref().unwrap().stats().0,
1485 1,
1486 "only A's texture is resident"
1487 );
1488
1489 let pins_b = [scene_b];
1491 for _ in 0..MAX_DRIVE_SPINS {
1492 drive_once(
1493 &mut state,
1494 &mut backend,
1495 [0.0; 3],
1496 IDENTITY4,
1497 false,
1498 Some(&pins_b),
1499 );
1500 let r = state.scene_residency.as_ref().unwrap();
1501 if r.state(scene_b) == Some(SceneLoadState::Resident)
1502 && r.state(scene_a) == Some(SceneLoadState::Unloaded)
1503 {
1504 break;
1505 }
1506 std::thread::yield_now();
1507 }
1508 let r = state.scene_residency.as_ref().unwrap();
1509 assert_eq!(r.state(scene_b), Some(SceneLoadState::Resident));
1510 assert_eq!(r.progress(scene_b), Some(1.0));
1511 assert_eq!(r.state(scene_a), Some(SceneLoadState::Unloaded));
1512 assert_eq!(state.texture_streamer.as_ref().unwrap().stats().0, 1);
1513 assert_eq!(state.mesh_streamer.as_ref().unwrap().stats().0, 1);
1514 }
1515
1516 #[test]
1518 fn the_gate_stage_freezes_new_loads() {
1519 let (_recorded, mut backend) = recording_backend();
1520 let mut state = pooled_state(8);
1521 state.chunk_stream = Some(chunk_state(Arc::new(ConstChunk), 0, 0));
1522 state.pressure_stage = StreamPressureStage::Gate;
1523
1524 drive_once(&mut state, &mut backend, [0.0; 3], IDENTITY4, false, None);
1525 assert_eq!(state.texture_streamer.as_ref().unwrap().stats(), (0, 0, 2));
1526 assert_eq!(state.mesh_streamer.as_ref().unwrap().stats(), (0, 0, 2));
1527 assert_eq!(
1528 state.chunk_stream.as_ref().unwrap().streamer.stats(),
1529 (0, 0)
1530 );
1531 }
1532
1533 #[test]
1536 fn the_evict_stage_keeps_planning() {
1537 let (_recorded, mut backend) = recording_backend();
1538 let mut state = pooled_state(8);
1539 state.pressure_stage = StreamPressureStage::Evict;
1540 drive_once(&mut state, &mut backend, [0.0; 3], IDENTITY4, false, None);
1541 assert!(state.texture_streamer.as_ref().unwrap().stats().2 < 2);
1542 assert!(state.mesh_streamer.as_ref().unwrap().stats().2 < 2);
1543 }
1544
1545 #[test]
1548 fn completed_loads_are_uploaded_to_the_backend() {
1549 let (recorded, mut backend) = recording_backend();
1550 let mut state = pooled_state(8);
1551 drive_until(&mut state, &mut backend, [0.0; 3], |s| {
1552 s.texture_streamer.as_ref().unwrap().stats().0 == 2
1553 && s.mesh_streamer.as_ref().unwrap().stats().0 == 2
1554 });
1555
1556 let s = recorded.lock().unwrap();
1557 assert!(s.saw(&Call::UpdateTextureSlot {
1558 slot: 0,
1559 w: 1,
1560 h: 1
1561 }));
1562 assert!(s.saw(&Call::UpdateTextureSlot {
1563 slot: 1,
1564 w: 1,
1565 h: 1
1566 }));
1567 assert!(s.saw(&Call::UploadMesh {
1568 draw_idx: 10,
1569 vertices: 3,
1570 indices: 3,
1571 }));
1572 assert!(s.saw(&Call::UploadMesh {
1573 draw_idx: 11,
1574 vertices: 3,
1575 indices: 3,
1576 }));
1577 }
1578
1579 #[test]
1582 fn a_streamed_mesh_without_a_draw_slot_uploads_nothing() {
1583 let (recorded, mut backend) = recording_backend();
1584 let mut state = pooled_state(8);
1585 state.mesh_stream_draw_indices.clear();
1586 drive_until(&mut state, &mut backend, [0.0; 3], |s| {
1587 s.mesh_streamer.as_ref().unwrap().stats().0 == 2
1588 });
1589 assert!(
1590 !recorded
1591 .lock()
1592 .unwrap()
1593 .calls
1594 .iter()
1595 .any(|c| matches!(c, Call::UploadMesh { .. }))
1596 );
1597 }
1598
1599 #[test]
1602 fn moving_the_camera_evicts_the_now_distant_item_over_the_cap() {
1603 let (recorded, mut backend) = recording_backend();
1604 let mut state = pooled_state(1);
1606 drive_until(&mut state, &mut backend, [0.0; 3], |s| {
1607 s.texture_streamer.as_ref().unwrap().stats().0 == 1
1608 && s.mesh_streamer.as_ref().unwrap().stats().0 == 1
1609 });
1610 recorded.lock().unwrap().calls.clear();
1611
1612 drive_once(
1614 &mut state,
1615 &mut backend,
1616 [100.0, 0.0, 0.0],
1617 IDENTITY4,
1618 false,
1619 None,
1620 );
1621 let s = recorded.lock().unwrap();
1622 assert!(s.saw(&Call::EvictTextureSlot(0)), "{:?}", s.calls);
1623 assert!(s.saw(&Call::EvictMesh(10)), "{:?}", s.calls);
1624 }
1625
1626 #[test]
1627 fn without_chunk_streaming_the_view_and_camera_stay_absolute() {
1628 let (_recorded, mut backend) = recording_backend();
1629 let mut state = empty_state();
1630 let view = translation_view(-40.0, -5.0, 40.0);
1631 let cam = [40.0, 5.0, -40.0];
1632 assert_eq!(
1633 drive_once(&mut state, &mut backend, cam, view, false, None),
1634 (view, cam)
1635 );
1636 }
1637
1638 #[test]
1641 fn chunk_streaming_rebases_the_view_onto_the_camera_chunk() {
1642 let (_recorded, mut backend) = recording_backend();
1643 let mut state = empty_state();
1644 state.chunk_stream = Some(chunk_state(Arc::new(FailingChunk), 0, 0));
1645 let cam = [40.0, 5.0, -40.0];
1647 let view = translation_view(-40.0, -5.0, 40.0);
1648 let (out_view, out_cam) = drive_once(&mut state, &mut backend, cam, view, false, None);
1649
1650 let origin = [32.0, 0.0, -48.0];
1651 assert_eq!(out_cam, [8.0, 5.0, 8.0]);
1652 assert_eq!(
1653 out_view,
1654 crate::gfx::chunk_coord::camera_relative_view(view, cam, origin)
1655 );
1656 assert_ne!(out_view, view, "the rebase actually rewrote the view");
1657 assert_eq!(
1658 state.chunk_stream.as_ref().unwrap().origin_chunk,
1659 ChunkCoord::new(2, -3)
1660 );
1661 }
1662
1663 #[test]
1666 fn crossing_into_a_new_chunk_rebases_resident_chunk_models() {
1667 let (recorded, mut backend) = recording_backend();
1668 let mut state = empty_state();
1669 let mut cs = chunk_state(Arc::new(FailingChunk), 1, 1);
1670 cs.draws.insert(ChunkCoord::new(0, 0), 3);
1672 cs.draws.insert(ChunkCoord::new(1, 0), 4);
1673 state.chunk_stream = Some(cs);
1674
1675 drive_once(
1676 &mut state,
1677 &mut backend,
1678 [20.0, 0.0, 0.0],
1679 IDENTITY4,
1680 false,
1681 None,
1682 );
1683 {
1684 let s = recorded.lock().unwrap();
1685 assert!(s.saw(&Call::SetChunkModel(3)));
1686 assert!(s.saw(&Call::SetChunkModel(4)));
1687 }
1688 assert_eq!(
1689 state.chunk_stream.as_ref().unwrap().origin_chunk,
1690 ChunkCoord::new(1, 0)
1691 );
1692
1693 recorded.lock().unwrap().calls.clear();
1694 drive_once(
1695 &mut state,
1696 &mut backend,
1697 [21.0, 0.0, 0.0],
1698 IDENTITY4,
1699 false,
1700 None,
1701 );
1702 assert!(
1703 !recorded
1704 .lock()
1705 .unwrap()
1706 .calls
1707 .iter()
1708 .any(|c| matches!(c, Call::SetChunkModel(_)))
1709 );
1710 }
1711
1712 #[test]
1715 fn chunks_leaving_the_view_window_are_removed_from_the_backend() {
1716 let (recorded, mut backend) = recording_backend();
1717 let mut state = empty_state();
1718 let mut cs = chunk_state(Arc::new(FailingChunk), 0, 0);
1719 cs.draws.insert(ChunkCoord::new(0, 0), 9);
1721 state.chunk_stream = Some(cs);
1722
1723 drive_once(&mut state, &mut backend, [0.0; 3], IDENTITY4, false, None);
1725 drive_once(
1727 &mut state,
1728 &mut backend,
1729 [800.0, 0.0, 0.0],
1730 IDENTITY4,
1731 false,
1732 None,
1733 );
1734
1735 assert!(recorded.lock().unwrap().saw(&Call::RemoveChunkMesh(9)));
1736 assert!(
1737 !state
1738 .chunk_stream
1739 .as_ref()
1740 .unwrap()
1741 .draws
1742 .contains_key(&ChunkCoord::new(0, 0))
1743 );
1744 }
1745
1746 #[test]
1749 fn a_generated_chunk_is_added_and_its_draw_slot_tracked() {
1750 let (recorded, mut backend) = recording_backend();
1751 let mut state = empty_state();
1752 state.chunk_stream = Some(chunk_state(Arc::new(ConstChunk), 0, 0));
1753 drive_until(&mut state, &mut backend, [0.0; 3], |s| {
1754 s.chunk_stream.as_ref().unwrap().streamer.stats().0 == 1
1755 });
1756
1757 let cs = state.chunk_stream.as_ref().unwrap();
1758 assert_eq!(cs.draws.get(&ChunkCoord::new(0, 0)), Some(&0));
1759 assert!(recorded.lock().unwrap().saw(&Call::AddChunkMesh));
1760 }
1761
1762 #[test]
1763 fn a_step_without_parked_state_publishes_no_view() {
1764 let mut w = StepWorld::new();
1765 assert_eq!(w.step(), StepResult::Continue);
1766 assert!(w.resources.get::<CameraRelativeView>().is_none());
1767 }
1768
1769 #[test]
1772 fn a_step_without_a_backend_still_publishes_the_absolute_view() {
1773 let view = translation_view(-1.0, -2.0, -3.0);
1774 let mut w = StepWorld::new().with_camera([1.0, 2.0, 3.0], view);
1775 w.resources.insert(empty_state());
1776
1777 assert_eq!(w.step(), StepResult::Continue);
1778 assert_eq!(w.view().cam_pos, [1.0, 2.0, 3.0]);
1779 assert_eq!(w.view().view, view);
1780 assert_eq!(w.parked_state().frame_count, 0, "no frame was driven");
1781 }
1782
1783 #[test]
1784 fn a_step_without_a_camera_publishes_the_identity_view() {
1785 let mut w = StepWorld::new();
1786 w.resources.insert(empty_state());
1787 w.park_render_queues();
1788
1789 w.step();
1790 assert_eq!(w.view().view, IDENTITY4);
1791 assert_eq!(w.view().cam_pos, [0.0; 3]);
1792 }
1793
1794 #[test]
1797 fn a_step_drives_the_pools_and_reparks_the_queues() {
1798 let mut w = StepWorld::new().with_camera([0.0; 3], IDENTITY4);
1799 w.resources.insert(pooled_state(8));
1800 w.park_render_queues();
1801
1802 w.step();
1803 assert!(
1804 w.resources
1805 .get::<crate::ecs::ActiveRenderQueues>()
1806 .is_some_and(|slot| slot.0.is_some())
1807 );
1808 assert_eq!(w.parked_state().frame_count, 1);
1809 assert!(
1810 w.parked_state()
1811 .texture_streamer
1812 .as_ref()
1813 .unwrap()
1814 .stats()
1815 .2
1816 < 2,
1817 "the pools were driven"
1818 );
1819 }
1820
1821 #[test]
1824 fn an_opaque_overlay_suspends_streaming_without_consuming_the_frame() {
1825 let mut w = StepWorld::new().with_camera([0.0; 3], IDENTITY4);
1826 w.resources.insert(pooled_state(8));
1827 w.park_render_queues();
1828 w.resources.insert(OverlayFrame {
1829 world_hidden: true,
1830 ..Default::default()
1831 });
1832
1833 w.step();
1834 assert_eq!(
1835 w.parked_state().texture_streamer.as_ref().unwrap().stats(),
1836 (0, 0, 2)
1837 );
1838 assert!(w.resources.get::<OverlayFrame>().is_some());
1839 }
1840
1841 #[test]
1843 fn ram_pressure_is_sampled_only_on_the_throttled_cadence() {
1844 let mut w = StepWorld::new();
1845 let mut state = empty_state();
1846 state.frame_count = 1;
1847 w.resources.insert(state);
1848 w.resources
1851 .insert(crate::app::budget::MemoryBudget::compute(None, 1));
1852
1853 w.step();
1854 assert!(w.resources.get::<StreamingPressure>().is_none());
1855 }
1856
1857 #[test]
1860 fn ram_pressure_is_not_sampled_without_a_memory_budget() {
1861 let mut w = StepWorld::new();
1862 w.resources.insert(empty_state());
1863 w.step();
1864 assert!(w.resources.get::<StreamingPressure>().is_none());
1865 }
1866
1867 #[test]
1868 fn an_rss_sample_over_the_memory_budget_publishes_engaged_pressure() {
1869 let mut w = StepWorld::new();
1870 w.resources.insert(empty_state());
1871 w.resources
1872 .insert(crate::app::budget::MemoryBudget::compute(None, 1));
1873
1874 w.step();
1875 match crate::app::sysmem::process_resident_bytes() {
1877 Some(_) => {
1878 let p = w.resources.get::<StreamingPressure>().expect("published");
1879 assert!(p.under_pressure);
1880 assert_eq!(p.budget_bytes, 1024 * 1024);
1881 assert_ne!(w.parked_state().pressure_stage, StreamPressureStage::None);
1882 }
1883 None => assert!(w.resources.get::<StreamingPressure>().is_none()),
1884 }
1885 }
1886}