Skip to main content

concinnity_core/gfx/
mesh_seed.rs

1//! Shrinkable seed VRAM: planning + buffer compaction for streamed mesh
2//! geometry.
3//!
4//! Without this, `build_draw_list` bakes every streamed mesh into the shared
5//! vertex/index buffers, so the buffers are sized for the *whole* streamed set
6//! and streaming never shrinks GPU memory. The shrinkable-seed path instead
7//! keeps only the resident geometry baked in and reserves a smaller `seed`
8//! headroom for streamed meshes -- sized to the cap-many largest meshes the
9//! residency cap permits. The streamer places meshes into the headroom on
10//! upload and tolerates a transient `alloc` miss while freed regions await
11//! their retire frame.
12//!
13//! This is pure policy + buffer math: no backend types, no I/O, no threads.
14
15use crate::gfx::mesh_payload::Vertex;
16use crate::gfx::render_types::{DrawObject, InstancedCluster};
17use alloc::vec::Vec;
18
19const VERTEX_STRIDE: usize = core::mem::size_of::<Vertex>();
20const INDEX_STRIDE: usize = core::mem::size_of::<u32>();
21
22/// The streaming headroom reserved in the shared vertex / index buffers, in
23/// bytes. After init the renderer seeds the mesh sub-allocators with this one
24/// block instead of the individual build-time regions, so the buffers shrink
25/// from "every streamed mesh at once" to "the cap-many resident at once".
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub struct MeshSeedRegion {
28    /// Byte offset of the streaming block in the shared vertex buffer.
29    pub vtx_offset: u64,
30    /// Byte size of the streaming block in the shared vertex buffer.
31    pub vtx_bytes: u64,
32    /// Byte offset of the streaming block in the shared index buffer.
33    pub idx_offset: u64,
34    /// Byte size of the streaming block in the shared index buffer.
35    pub idx_bytes: u64,
36}
37
38/// Decide the seed headroom (in bytes) for a streamed mesh set, or `None` when
39/// no shrink is possible.
40///
41/// `mesh_byte_sizes[i] = (vertex_bytes, index_bytes)` for streamed mesh `i`,
42/// where `index_bytes` is the *u32* shared-buffer stride (per-mesh `u16`
43/// indices are widened on upload). `cap` is `StreamingConfig::mesh_cap`.
44///
45/// The seed is sized to hold the `residency` largest meshes at once, where
46/// `residency = cap + margin` capped at the mesh count. The cap-many floor
47/// guarantees the steady-state resident set (the planner keeps at most `cap`
48/// resident) always fits, so a load can never *permanently* miss; the margin
49/// absorbs the transient where an eviction's freed region still awaits its
50/// retire frame while the replacement loads. Sizing each buffer by the
51/// largest-`residency` of *that* buffer's per-mesh bytes is a safe independent
52/// upper bound for each buffer.
53///
54/// Returns `None` (the caller keeps the full set baked in, as before) when the
55/// cap -- plus margin -- can already hold every streamed mesh, so there is no
56/// VRAM to reclaim.
57pub fn plan_seed_bytes(mesh_byte_sizes: &[(u64, u64)], cap: usize) -> Option<(u64, u64)> {
58    let n = mesh_byte_sizes.len();
59    let cap = cap.max(1);
60    if cap >= n {
61        // Every streamed mesh can be resident at once: the full set is already
62        // the minimal seed.
63        return None;
64    }
65    let margin = (cap / 4).max(1);
66    let residency = (cap + margin).min(n);
67    if residency >= n {
68        // The margin already covers the whole set -- shrinking would not help.
69        return None;
70    }
71    let mut vtx: Vec<u64> = mesh_byte_sizes.iter().map(|&(v, _)| v).collect();
72    let mut idx: Vec<u64> = mesh_byte_sizes.iter().map(|&(_, i)| i).collect();
73    // Largest `residency` of each, summed independently. Descending sort, then
74    // take the front.
75    vtx.sort_unstable_by(|a, b| b.cmp(a));
76    idx.sort_unstable_by(|a, b| b.cmp(a));
77    let seed_vtx: u64 = vtx.iter().take(residency).sum();
78    let seed_idx: u64 = idx.iter().take(residency).sum();
79    Some((seed_vtx, seed_idx))
80}
81
82// A geometry region to relocate: its vertex byte offset and count in the
83// source buffer, its LOD0 (index offset, count), and each alternate LOD's
84// (index offset, count) in input order.
85struct Region<'a> {
86    v_off_bytes: usize,
87    v_count: usize,
88    lod0: (usize, usize),
89    alts: &'a [(usize, usize)],
90}
91
92// Copy one geometry region -- its vertices plus its LOD0 and alternate index
93// ranges -- into the growing destination buffers, rebasing the region's
94// absolute indices onto the moved vertex base. Returns the region's new
95// vertex byte offset, its new LOD0 index element offset, and the new element
96// offset of each alternate (in input order).
97fn relocate_region(
98    src_v: &[Vertex],
99    src_i: &[u32],
100    dst_v: &mut Vec<Vertex>,
101    dst_i: &mut Vec<u32>,
102    region: Region,
103) -> (usize, usize, Vec<usize>) {
104    let Region {
105        v_off_bytes,
106        v_count,
107        lod0,
108        alts,
109    } = region;
110    let old_vbase = v_off_bytes / VERTEX_STRIDE;
111    let new_vbase = dst_v.len();
112    dst_v.extend_from_slice(&src_v[old_vbase..old_vbase + v_count]);
113    // Indices are absolute into the shared vertex buffer; moving the vertices
114    // from old_vbase to new_vbase shifts every index by the same delta. Use
115    // i64 so a cluster relocated *forward* (its geometry sat behind the props
116    // it now trails) rebases correctly too.
117    let delta = new_vbase as i64 - old_vbase as i64;
118    let rebase = |i: u32| -> u32 { (i as i64 + delta) as u32 };
119
120    let (i0_off, i0_count) = lod0;
121    let new_i0_off = dst_i.len();
122    for &idx in &src_i[i0_off..i0_off + i0_count] {
123        dst_i.push(rebase(idx));
124    }
125    let mut new_alt_offsets = Vec::with_capacity(alts.len());
126    for &(a_off, a_count) in alts {
127        let new_a_off = dst_i.len();
128        for &idx in &src_i[a_off..a_off + a_count] {
129            dst_i.push(rebase(idx));
130        }
131        new_alt_offsets.push(new_a_off);
132    }
133    (new_vbase * VERTEX_STRIDE, new_i0_off, new_alt_offsets)
134}
135
136/// Rewrite the shared vertex / index buffers so only resident geometry is
137/// baked in, then append a zeroed seed headroom for streamed meshes.
138///
139/// Every resident `DrawObject` / `InstancedCluster` offset (and LOD-alternate
140/// offset) is rewritten to its new place, rebasing its absolute indices onto
141/// the moved vertex region. Each streamed draw (`streamed[i] == true`) is
142/// marked non-resident with placeholder offsets and its geometry is *not*
143/// copied -- it lives in the streamer's payload source and is uploaded on
144/// demand into the headroom.
145///
146/// Run before backend init so the GPU buffers are created at the compacted
147/// size and the RT acceleration structure (built over resident draws) sees the
148/// final offsets. Returns the headroom region to seed into the mesh
149/// sub-allocators.
150pub fn compact_for_streaming(
151    vertices: &mut Vec<Vertex>,
152    indices: &mut Vec<u32>,
153    draw_objects: &mut [DrawObject],
154    clusters: &mut [InstancedCluster],
155    streamed: &[bool],
156    seed_vtx_bytes: u64,
157    seed_idx_bytes: u64,
158) -> MeshSeedRegion {
159    let mut new_v: Vec<Vertex> = Vec::with_capacity(vertices.len());
160    let mut new_i: Vec<u32> = Vec::with_capacity(indices.len());
161
162    for (i, obj) in draw_objects.iter_mut().enumerate() {
163        if streamed.get(i).copied().unwrap_or(false) {
164            // Streamed draws are uploaded on demand; their geometry is not
165            // baked in. Placeholder offsets -- `upload_mesh` assigns real ones
166            // from the seeded headroom. (Alternates are already stripped from
167            // streamable draws upstream; clear defensively so no stale offset
168            // survives into the smaller buffer.)
169            obj.vertex_offset = 0;
170            obj.index_offset = 0;
171            obj.resident = false;
172            obj.lod_alternates.clear();
173            continue;
174        }
175        let alts: Vec<(usize, usize)> = obj
176            .lod_alternates
177            .iter()
178            .map(|s| (s.index_offset, s.index_count))
179            .collect();
180        let (new_v_off, new_i_off, new_alt_offs) = relocate_region(
181            vertices,
182            indices,
183            &mut new_v,
184            &mut new_i,
185            Region {
186                v_off_bytes: obj.vertex_offset,
187                v_count: obj.vertex_count,
188                lod0: (obj.index_offset, obj.index_count),
189                alts: &alts,
190            },
191        );
192        obj.vertex_offset = new_v_off;
193        obj.index_offset = new_i_off;
194        for (slice, new_off) in obj.lod_alternates.iter_mut().zip(new_alt_offs) {
195            slice.index_offset = new_off;
196        }
197    }
198
199    // Clusters never stream, but their geometry sits after the props in the
200    // shared buffer, so removing streamed-prop gaps shifts every cluster.
201    for c in clusters.iter_mut() {
202        let alts: Vec<(usize, usize)> = c
203            .lod_alternates
204            .iter()
205            .map(|s| (s.index_offset, s.index_count))
206            .collect();
207        let (new_v_off, new_i_off, new_alt_offs) = relocate_region(
208            vertices,
209            indices,
210            &mut new_v,
211            &mut new_i,
212            Region {
213                v_off_bytes: c.vertex_offset,
214                v_count: c.vertex_count,
215                lod0: (c.index_offset, c.index_count),
216                alts: &alts,
217            },
218        );
219        c.vertex_offset = new_v_off;
220        c.index_offset = new_i_off;
221        for (slice, new_off) in c.lod_alternates.iter_mut().zip(new_alt_offs) {
222            slice.index_offset = new_off;
223        }
224    }
225
226    // Append the zeroed seed headroom. Contents are irrelevant -- a streamed
227    // draw is skipped until its geometry is uploaded over this region -- only
228    // the size matters, so the buffers are born at resident + headroom bytes.
229    let vtx_offset = (new_v.len() * VERTEX_STRIDE) as u64;
230    let idx_offset = (new_i.len() * INDEX_STRIDE) as u64;
231    let zero_v = Vertex {
232        pos: [0.0; 3],
233        normal: [0.0; 3],
234        tangent: [0.0; 3],
235        color: [0.0; 3],
236        uv: [0.0; 2],
237    };
238    let seed_v_count = (seed_vtx_bytes as usize) / VERTEX_STRIDE;
239    let seed_i_count = (seed_idx_bytes as usize) / INDEX_STRIDE;
240    new_v.extend(core::iter::repeat_n(zero_v, seed_v_count));
241    new_i.extend(core::iter::repeat_n(0u32, seed_i_count));
242
243    *vertices = new_v;
244    *indices = new_i;
245    MeshSeedRegion {
246        vtx_offset,
247        vtx_bytes: seed_vtx_bytes,
248        idx_offset,
249        idx_bytes: seed_idx_bytes,
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256    use crate::gfx::render_types::{LodSlice, MaterialUniforms};
257    use alloc::vec;
258
259    fn vtx(x: f32) -> Vertex {
260        Vertex {
261            pos: [x, 0.0, 0.0],
262            normal: [0.0, 1.0, 0.0],
263            tangent: [1.0, 0.0, 0.0],
264            color: [1.0, 1.0, 1.0],
265            uv: [0.0, 0.0],
266        }
267    }
268
269    // Append `n` vertices (tagged by `tag` so a relocated region is
270    // identifiable) at the current end and return (vertex_byte_offset, base).
271    fn push_verts(v: &mut Vec<Vertex>, n: usize, tag: f32) -> (usize, u32) {
272        let base = v.len() as u32;
273        for k in 0..n {
274            v.push(vtx(tag + k as f32 / 100.0));
275        }
276        (base as usize * VERTEX_STRIDE, base)
277    }
278
279    // Append a triangle-fan-ish index list referencing the region's own
280    // vertices (absolute, base + 0..n) and return (index_offset, count).
281    fn push_idx(i: &mut Vec<u32>, base: u32, n: usize) -> (usize, usize) {
282        let off = i.len();
283        for k in 0..n {
284            i.push(base + (k as u32 % 3));
285        }
286        (off, n)
287    }
288
289    fn draw(
290        v_off: usize,
291        v_count: usize,
292        i_off: usize,
293        i_count: usize,
294        resident: bool,
295        lods: Vec<LodSlice>,
296    ) -> DrawObject {
297        DrawObject {
298            vertex_offset: v_off,
299            vertex_count: v_count,
300            index_offset: i_off,
301            index_count: i_count,
302            shader_bucket: 0,
303            base_vertex: 0,
304            geometry_generation: 0,
305            model: [[0.0; 4]; 4],
306            texture_slot: 0,
307            normal_map_slot: 0,
308            material: MaterialUniforms::DEFAULT,
309            visible: true,
310            resident,
311            bb_min: [0.0; 3],
312            bb_max: [0.0; 3],
313            cull_distance: 0.0,
314            lod_alternates: lods,
315        }
316    }
317
318    fn cluster(v_off: usize, v_count: usize, i_off: usize, i_count: usize) -> InstancedCluster {
319        InstancedCluster {
320            vertex_offset: v_off,
321            vertex_count: v_count,
322            index_offset: i_off,
323            index_count: i_count,
324            texture_slot: 0,
325            normal_map_slot: 0,
326            material: MaterialUniforms::DEFAULT,
327            cluster_bb_min: [0.0; 3],
328            cluster_bb_max: [0.0; 3],
329            local_bb_min: [0.0; 3],
330            local_bb_max: [0.0; 3],
331            cull_distance: 0.0,
332            instances: vec![[[0.0; 4]; 4]],
333            lod_alternates: Vec::new(),
334        }
335    }
336
337    #[test]
338    fn plan_returns_none_when_cap_holds_every_mesh() {
339        // 3 meshes, cap 4 -> all can be resident, nothing to shrink.
340        let sizes = vec![(100, 40), (100, 40), (100, 40)];
341        assert_eq!(plan_seed_bytes(&sizes, 4), None);
342        // cap == count is also "no shrink".
343        assert_eq!(plan_seed_bytes(&sizes, 3), None);
344    }
345
346    #[test]
347    fn plan_returns_none_when_margin_covers_the_set() {
348        // 5 meshes, cap 4 -> margin = max(1,1) = 1, residency = 5 = count.
349        let sizes = vec![(100, 40); 5];
350        assert_eq!(plan_seed_bytes(&sizes, 4), None);
351    }
352
353    #[test]
354    fn plan_sizes_seed_to_the_largest_residency_meshes() {
355        // 8 uniform meshes, cap 4 -> margin = 1, residency = 5.
356        let sizes = vec![(100u64, 40u64); 8];
357        let (sv, si) = plan_seed_bytes(&sizes, 4).expect("shrink");
358        assert_eq!(sv, 100 * 5);
359        assert_eq!(si, 40 * 5);
360        // The seed is strictly smaller than the full set (8 meshes).
361        assert!(sv < 100 * 8);
362        assert!(si < 40 * 8);
363    }
364
365    #[test]
366    fn plan_picks_the_biggest_meshes_independently_per_buffer() {
367        // Skewed sizes: the largest-vtx meshes differ from the largest-idx ones.
368        // cap 1 -> margin = 1, residency = 2.
369        let sizes = vec![
370            (1000, 4), // big vtx, small idx
371            (4, 1000), // small vtx, big idx
372            (10, 10),  // small both
373            (10, 10),
374        ];
375        let (sv, si) = plan_seed_bytes(&sizes, 1).expect("shrink");
376        // top-2 vtx: 1000 + 10; top-2 idx: 1000 + 10
377        assert_eq!(sv, 1010);
378        assert_eq!(si, 1010);
379    }
380
381    #[test]
382    fn compact_removes_streamed_gaps_and_rewrites_resident_offsets() {
383        // Layout: [resident A][streamed B][resident C], then [cluster D].
384        let mut v: Vec<Vertex> = Vec::new();
385        let mut i: Vec<u32> = Vec::new();
386        let (a_voff, a_base) = push_verts(&mut v, 4, 1.0);
387        let (a_ioff, a_ic) = push_idx(&mut i, a_base, 6);
388        let (b_voff, b_base) = push_verts(&mut v, 10, 2.0);
389        let (b_ioff, b_ic) = push_idx(&mut i, b_base, 12);
390        let (c_voff, c_base) = push_verts(&mut v, 5, 3.0);
391        let (c_ioff, c_ic) = push_idx(&mut i, c_base, 9);
392        let (d_voff, d_base) = push_verts(&mut v, 6, 4.0);
393        let (d_ioff, d_ic) = push_idx(&mut i, d_base, 6);
394
395        let mut draws = vec![
396            draw(a_voff, 4, a_ioff, a_ic, true, vec![]),
397            draw(b_voff, 10, b_ioff, b_ic, true, vec![]),
398            draw(c_voff, 5, c_ioff, c_ic, true, vec![]),
399        ];
400        // Mark only B as streamed; A and C are resident.
401        let streamed = [false, true, false];
402        let mut clusters = vec![cluster(d_voff, 6, d_ioff, d_ic)];
403
404        // Snapshot the geometry we expect to survive (A, C, D). `Vertex` has no
405        // `PartialEq`, so compare by the position tag `push_verts` stamped.
406        let pos = |s: &[Vertex]| -> Vec<[f32; 3]> { s.iter().map(|x| x.pos).collect() };
407        let a_verts = pos(&v[a_base as usize..a_base as usize + 4]);
408        let c_verts = pos(&v[c_base as usize..c_base as usize + 5]);
409        let d_verts = pos(&v[d_base as usize..d_base as usize + 6]);
410
411        let region = compact_for_streaming(
412            &mut v,
413            &mut i,
414            &mut draws,
415            &mut clusters,
416            &streamed,
417            /*seed_vtx*/ 0,
418            /*seed_idx*/ 0,
419        );
420
421        // Streamed B is non-resident with placeholder offsets.
422        assert!(!draws[1].resident);
423        assert_eq!(draws[1].vertex_offset, 0);
424        assert_eq!(draws[1].index_offset, 0);
425
426        // Resident A keeps its (now front-of-buffer) offsets; geometry intact.
427        assert!(draws[0].resident);
428        assert_eq!(draws[0].vertex_offset, 0);
429        let a0 = draws[0].vertex_offset / VERTEX_STRIDE;
430        assert_eq!(pos(&v[a0..a0 + 4]), a_verts);
431
432        // Resident C moved up to fill B's gap; its indices still address its
433        // own (relocated) vertices.
434        let c0 = draws[2].vertex_offset / VERTEX_STRIDE;
435        assert_eq!(pos(&v[c0..c0 + 5]), c_verts);
436        for k in 0..c_ic {
437            let idx = i[draws[2].index_offset + k] as usize;
438            assert!(
439                idx >= c0 && idx < c0 + 5,
440                "C index {} out of its region",
441                idx
442            );
443        }
444
445        // Cluster D relocated; indices address its own vertices.
446        let d0 = clusters[0].vertex_offset / VERTEX_STRIDE;
447        assert_eq!(pos(&v[d0..d0 + 6]), d_verts);
448        for k in 0..d_ic {
449            let idx = i[clusters[0].index_offset + k] as usize;
450            assert!(
451                idx >= d0 && idx < d0 + 6,
452                "D index {} out of its region",
453                idx
454            );
455        }
456
457        // The buffer shrank by exactly B's vertex + index region.
458        assert_eq!(v.len(), 4 + 5 + 6); // A + C + D, no headroom this test
459        assert_eq!(i.len(), a_ic + c_ic + d_ic);
460        // Zero headroom requested -> region sits at the compacted tail.
461        assert_eq!(region.vtx_offset, (v.len() * VERTEX_STRIDE) as u64);
462        assert_eq!(region.vtx_bytes, 0);
463    }
464
465    #[test]
466    fn compact_appends_seed_headroom_after_resident_geometry() {
467        let mut v: Vec<Vertex> = Vec::new();
468        let mut i: Vec<u32> = Vec::new();
469        let (a_voff, a_base) = push_verts(&mut v, 3, 1.0);
470        let (a_ioff, a_ic) = push_idx(&mut i, a_base, 3);
471        let mut draws = vec![draw(a_voff, 3, a_ioff, a_ic, true, vec![])];
472        let mut clusters: Vec<InstancedCluster> = Vec::new();
473        let streamed = [false];
474
475        let seed_v_bytes = (10 * VERTEX_STRIDE) as u64;
476        let seed_i_bytes = (24 * INDEX_STRIDE) as u64;
477        let region = compact_for_streaming(
478            &mut v,
479            &mut i,
480            &mut draws,
481            &mut clusters,
482            &streamed,
483            seed_v_bytes,
484            seed_i_bytes,
485        );
486
487        // Headroom begins right after the 3 resident vertices / indices.
488        assert_eq!(region.vtx_offset, (3 * VERTEX_STRIDE) as u64);
489        assert_eq!(region.idx_offset, (3 * INDEX_STRIDE) as u64);
490        assert_eq!(region.vtx_bytes, seed_v_bytes);
491        assert_eq!(region.idx_bytes, seed_i_bytes);
492        // Buffers are sized for resident + headroom.
493        assert_eq!(v.len(), 3 + 10);
494        assert_eq!(i.len(), 3 + 24);
495        // Headroom start is vertex-aligned (offset is a whole multiple of stride).
496        assert_eq!(region.vtx_offset as usize % VERTEX_STRIDE, 0);
497    }
498
499    #[test]
500    fn compact_relocates_lod_alternate_index_ranges() {
501        // [streamed A][resident B(with one LOD alt)].
502        let mut v: Vec<Vertex> = Vec::new();
503        let mut i: Vec<u32> = Vec::new();
504        let (a_voff, a_base) = push_verts(&mut v, 8, 1.0);
505        let (a_ioff, a_ic) = push_idx(&mut i, a_base, 12);
506        let (b_voff, b_base) = push_verts(&mut v, 4, 2.0);
507        let (b_ioff, b_ic) = push_idx(&mut i, b_base, 6); // LOD0
508        let (b_alt_off, b_alt_ic) = push_idx(&mut i, b_base, 3); // LOD1 alt
509
510        let mut draws = vec![
511            draw(a_voff, 8, a_ioff, a_ic, true, vec![]),
512            draw(
513                b_voff,
514                4,
515                b_ioff,
516                b_ic,
517                true,
518                vec![LodSlice {
519                    index_offset: b_alt_off,
520                    index_count: b_alt_ic,
521                    switch_distance: 10.0,
522                }],
523            ),
524        ];
525        let mut clusters: Vec<InstancedCluster> = Vec::new();
526        let streamed = [true, false];
527
528        compact_for_streaming(&mut v, &mut i, &mut draws, &mut clusters, &streamed, 0, 0);
529
530        let b0 = draws[1].vertex_offset / VERTEX_STRIDE;
531        // LOD0 and the alternate both address B's relocated vertices.
532        for k in 0..b_ic {
533            let idx = i[draws[1].index_offset + k] as usize;
534            assert!(idx >= b0 && idx < b0 + 4);
535        }
536        let alt = draws[1].lod_alternates[0];
537        for k in 0..alt.index_count {
538            let idx = i[alt.index_offset + k] as usize;
539            assert!(idx >= b0 && idx < b0 + 4);
540        }
541        // The alternate's switch distance is preserved.
542        assert_eq!(alt.switch_distance, 10.0);
543    }
544}