Ticket 013 — rasterise every pending mesh card into its assigned atlas slot. Called once per frame before the HW probe trace samples the atlas. Drains `scene.pending_card_captures`; subsequent frames with no new meshes are free.
(
&mut self,
scene: &mut crate::scene::SceneGraph,
encoder: &mut wgpu::CommandEncoder,
)
| 6638 | /// trace samples the atlas. Drains `scene.pending_card_captures`; |
| 6639 | /// subsequent frames with no new meshes are free. |
| 6640 | fn capture_pending_mesh_cards( |
| 6641 | &mut self, |
| 6642 | scene: &mut crate::scene::SceneGraph, |
| 6643 | encoder: &mut wgpu::CommandEncoder, |
| 6644 | ) { |
| 6645 | if scene.pending_card_captures.is_empty() { |
| 6646 | return; |
| 6647 | } |
| 6648 | // Rate-limit: at 6 axes per mesh one encoder fills up after a |
| 6649 | // few hundred render passes (observed hang on Sponza's 405 × |
| 6650 | // 6 = 2430-pass batch). Drain in chunks and let subsequent |
| 6651 | // frames continue the work — Sponza finishes in a few frames. |
| 6652 | const CAPTURE_MAX_PER_FRAME: usize = 20; |
| 6653 | let take = scene.pending_card_captures.len().min(CAPTURE_MAX_PER_FRAME); |
| 6654 | let pending: Vec<f64> = scene.pending_card_captures.drain(..take).collect(); |
| 6655 | |
| 6656 | // Pre-compute per-slot world-space normals for the whole batch. |
| 6657 | // `slot_meta` is a Vec<[f32;4]> where entry `first_slot + axis` |
| 6658 | // carries the card face's world-space normal (xyz) + unused w. |
| 6659 | // Uploaded in one `queue.write_buffer` after the capture loop |
| 6660 | // so the card-lighting compute pass sees the populated slots. |
| 6661 | let mut slot_meta_updates: Vec<(u32, CardSlotMetaCpu)> = Vec::new(); |
| 6662 | |
| 6663 | for handle in pending { |
| 6664 | let (first_slot, bmin, bmax, transform, has_tex, bc, tex_idx, has_em, em_factor, em_idx, vb_ptr, ib_ptr, index_count) = { |
| 6665 | let Some(node) = scene.nodes.get(handle) else { continue; }; |
| 6666 | let Some(first_slot) = node.card_first_slot else { continue; }; |
| 6667 | if first_slot + CARD_AXES_PER_MESH > CARD_MAX_SLOTS { |
| 6668 | continue; |
| 6669 | } |
| 6670 | let Some(vb) = node.gpu_vb.as_ref() else { continue; }; |
| 6671 | let Some(ib) = node.gpu_ib.as_ref() else { continue; }; |
| 6672 | let has_tex = node.material.texture_idx != 0; |
| 6673 | let em_idx = node.material.emissive_texture_idx; |
| 6674 | let has_em = em_idx != 0; |
| 6675 | ( |
| 6676 | first_slot, |
| 6677 | node.bounds_min, |
| 6678 | node.bounds_max, |
| 6679 | node.transform, |
| 6680 | has_tex, |
| 6681 | node.material.color, |
| 6682 | node.material.texture_idx, |
| 6683 | has_em, |
| 6684 | node.material.emissive, |
| 6685 | em_idx, |
| 6686 | vb.clone(), |
| 6687 | ib.clone(), |
| 6688 | node.gpu_index_count, |
| 6689 | ) |
| 6690 | }; |
| 6691 | if index_count == 0 { |
| 6692 | continue; |
| 6693 | } |
| 6694 | |
| 6695 | // Card textures + sampler binding stay the same across all |
| 6696 | // 6 axes, so build the bind groups once per mesh. |
| 6697 | let tex_view = if has_tex && (tex_idx as usize) < self.textures.len() { |
no test coverage detected