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(stages)) => {
365 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 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 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 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 let loads_frozen = self.pressure_stage.freezes_loads();
466
467 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 self.drive_shader_warmup(ops);
501
502 let world_hidden = world_hidden
506 && !self
507 .scene_residency
508 .as_ref()
509 .is_some_and(|r| r.any_loading());
510
511 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 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 if !world_hidden && let Some(streamer) = &mut self.mesh_streamer {
562 streamer.update_scores(cam_pos, self.frame_count);
563 if !loads_frozen {
564 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 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 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 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 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 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 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 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 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 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 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 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 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 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
918pub(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 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 struct ConstTexture;
980 impl PayloadSource for ConstTexture {
981 fn fetch(&self, _id: usize) -> Result<DecodedTexture, String> {
982 Ok(DecodedTexture {
983 image: crate::build::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 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 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 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 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 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 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 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 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 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 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 #[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 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 assert_eq!(d.heap_growth_bytes + d.outside_heap_growth_bytes, 0);
1233 }
1234
1235 #[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 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); assert_eq!(s.pressure_stage, StreamPressureStage::Evict);
1265 s.sample_pressure(Some(850), 1000);
1267 assert_eq!(s.pressure_stage, StreamPressureStage::Evict);
1268 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 assert!(s.sample_pressure(None, 1000).is_none());
1282 assert_eq!(s.pressure_stage, stage_before);
1283 }
1284
1285 #[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 #[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 #[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 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 #[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 #[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 assert!(state.texture_streamer.as_ref().unwrap().stats().2 < 2);
1432 assert!(state.mesh_streamer.as_ref().unwrap().stats().2 < 2);
1433 }
1434
1435 #[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 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 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 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 #[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 #[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 #[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 #[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 #[test]
1605 fn moving_the_camera_evicts_the_now_distant_item_over_the_cap() {
1606 let (recorded, mut backend) = recording_backend();
1607 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 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 #[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 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 #[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 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 #[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 cs.draws.insert(ChunkCoord::new(0, 0), 9);
1724 state.chunk_stream = Some(cs);
1725
1726 drive_once(&mut state, &mut backend, [0.0; 3], IDENTITY4, false, None);
1728 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 #[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 #[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 #[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 #[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 #[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 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 #[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 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}