1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
// src/metal/resources/geometry.rs
//
// Hot-reload rebuild of the shared static-mesh vertex + index buffers when
// re-imported `.glb` source no longer fits each draw's init-time slot.
#![deny(unsafe_op_in_unsafe_fn)]
use objc2_metal::{MTLBuffer as _, MTLResourceOptions};
use crate::gfx::mesh_payload::Vertex;
use crate::gfx::render_types::LodSlice;
use crate::metal::context::{MtlContext, bytes_of_slice};
impl MtlContext {
// Rebuild the shared static-mesh vertex + index buffers, swapping in
// new geometry for the draws named in `changes`. Driven by asset
// hot-reload (`cn debug` only) when a Mesh's re-imported `.glb` no
// longer fits in its init-time slot. Walks every `DrawObject` in
// order: for each draw in `changes`, the new vertices / indices /
// LOD alternates are appended to a fresh CPU buffer; for unchanged
// draws, the current geometry is read back from the live
// `vertex_buffer` / `index_buffer` (both `StorageModeShared` so the
// pointers are CPU-readable) and copied with index rebasing. New
// `MTLBuffer`s are created at the post-rebuild size and swapped in
// after `wait_idle` so no in-flight command buffer touches the old
// resource pair. Streaming sub-allocators (`geometry_alloc.mesh_vtx`,
// `geometry_alloc.mesh_idx`) are reset to the new buffer's full extent -- any
// streaming uploads in flight would be invalidated by the swap, so
// the caller is expected to gate this on a quiet renderer (the
// asset-hot-reload path already is -- it runs at frame start before
// the streaming poll).
pub(crate) fn rebuild_static_geometry(
&mut self,
changes: Vec<crate::gfx::backend::DrawGeometryUpdate>,
) -> Result<(), String> {
use std::collections::HashMap;
// Stop the GPU + CPU pipelines so we can safely read the old
// buffers and atomically swap. Costs a frame-time stall but only
// fires under `cn debug` and only when the source `.glb` size
// actually changed.
self.wait_idle();
let mut change_map: HashMap<usize, crate::gfx::backend::DrawGeometryUpdate> =
changes.into_iter().map(|c| (c.draw_idx, c)).collect();
// Read views over the current shared buffers. `StorageModeShared`
// means `contents()` is a CPU-addressable pointer aliasing the GPU
// data; safe after `wait_idle`.
let old_v_len = self.vertex_buffer.length() / std::mem::size_of::<Vertex>();
// SAFETY: the buffer is `StorageModeShared`, so `contents()` is a live CPU mapping of its
// bytes, and the length was derived from that buffer's own byte length divided by the
// element size. The preceding `wait_idle` means the GPU is not writing it.
let old_v_slice: &[Vertex] = unsafe {
let ptr = self.vertex_buffer.contents().as_ptr() as *const Vertex;
std::slice::from_raw_parts(ptr, old_v_len)
};
let old_i_len = self.index_buffer.length() / std::mem::size_of::<u32>();
// SAFETY: the buffer is `StorageModeShared`, so `contents()` is a live CPU mapping of its
// bytes, and the length was derived from that buffer's own byte length divided by the
// element size. The preceding `wait_idle` means the GPU is not writing it.
let old_i_slice: &[u32] = unsafe {
let ptr = self.index_buffer.contents().as_ptr() as *const u32;
std::slice::from_raw_parts(ptr, old_i_len)
};
let mut new_vertices: Vec<Vertex> = Vec::new();
let mut new_indices: Vec<u32> = Vec::new();
// Captured per-draw new layout (applied to `draw.objects` after the
// read-only walk to avoid aliasing `self`).
type DrawLayout = (usize, usize, usize, usize, i32, Vec<LodSlice>);
let mut new_layouts: Vec<DrawLayout> = Vec::with_capacity(self.draw.objects.len());
for (draw_idx, obj) in self.draw.objects.iter().enumerate() {
let new_v_byte_off = new_vertices.len() * std::mem::size_of::<Vertex>();
let new_i_elem_off = new_indices.len();
let new_base_u32 = new_vertices.len() as u32;
// base_vertex == 0 means "absolute indices" (the static draws);
// non-zero means "mesh-relative indices, GPU adds base_vertex
// at fetch time" (voxel chunks). We preserve each draw's
// semantics on rebuild.
let absolute_indices = obj.base_vertex == 0;
let new_base_vertex = if absolute_indices {
0
} else {
(new_v_byte_off / std::mem::size_of::<Vertex>()) as i32
};
if let Some(change) = change_map.remove(&draw_idx) {
new_vertices.extend_from_slice(&change.vertices);
if absolute_indices {
new_indices.extend(change.indices.iter().map(|i| u32::from(*i) + new_base_u32));
} else {
new_indices.extend(change.indices.iter().map(|i| u32::from(*i)));
}
let mut new_lods: Vec<LodSlice> = Vec::with_capacity(change.lod_alternates.len());
for (switch_distance, alt_idx) in &change.lod_alternates {
let alt_off = new_indices.len();
if absolute_indices {
new_indices.extend(alt_idx.iter().map(|i| u32::from(*i) + new_base_u32));
} else {
new_indices.extend(alt_idx.iter().map(|i| u32::from(*i)));
}
new_lods.push(LodSlice {
index_offset: alt_off,
index_count: alt_idx.len(),
switch_distance: *switch_distance,
});
}
new_layouts.push((
new_v_byte_off,
change.vertices.len(),
new_i_elem_off,
change.indices.len(),
new_base_vertex,
new_lods,
));
} else {
// Unchanged draw -- copy current geometry verbatim, rebasing
// indices onto the new vertex region.
let v_start = obj.vertex_offset / std::mem::size_of::<Vertex>();
let v_end = v_start + obj.vertex_count;
if v_end > old_v_slice.len() {
return Err(format!(
"rebuild_static_geometry: draw {} vertex region [{}, {}) out \
of bounds (buffer has {} vertices)",
draw_idx,
v_start,
v_end,
old_v_slice.len()
));
}
new_vertices.extend_from_slice(&old_v_slice[v_start..v_end]);
let old_base_u32 = if absolute_indices {
v_start as u32
} else {
obj.base_vertex as u32
};
let i_end = obj.index_offset + obj.index_count;
if i_end > old_i_slice.len() {
return Err(format!(
"rebuild_static_geometry: draw {} index region [{}, {}) out \
of bounds (buffer has {} indices)",
draw_idx,
obj.index_offset,
i_end,
old_i_slice.len()
));
}
if absolute_indices {
for &idx in &old_i_slice[obj.index_offset..i_end] {
new_indices.push(idx.wrapping_sub(old_base_u32) + new_base_u32);
}
} else {
new_indices.extend_from_slice(&old_i_slice[obj.index_offset..i_end]);
}
let mut new_lods: Vec<LodSlice> = Vec::with_capacity(obj.lod_alternates.len());
for slice in &obj.lod_alternates {
let alt_end = slice.index_offset + slice.index_count;
if alt_end > old_i_slice.len() {
return Err(format!(
"rebuild_static_geometry: draw {} LOD slice [{}, {}) out \
of bounds (buffer has {} indices)",
draw_idx,
slice.index_offset,
alt_end,
old_i_slice.len()
));
}
let alt_off = new_indices.len();
if absolute_indices {
for &idx in &old_i_slice[slice.index_offset..alt_end] {
new_indices.push(idx.wrapping_sub(old_base_u32) + new_base_u32);
}
} else {
new_indices.extend_from_slice(&old_i_slice[slice.index_offset..alt_end]);
}
new_lods.push(LodSlice {
index_offset: alt_off,
index_count: slice.index_count,
switch_distance: slice.switch_distance,
});
}
new_layouts.push((
new_v_byte_off,
obj.vertex_count,
new_i_elem_off,
obj.index_count,
new_base_vertex,
new_lods,
));
}
}
if !change_map.is_empty() {
tracing::warn!(
"rebuild_static_geometry: {} change(s) targeted draw indices not in \
draw_objects (ignored)",
change_map.len()
);
}
if new_vertices.is_empty() || new_indices.is_empty() {
return Err(
"rebuild_static_geometry: post-rebuild buffers would be empty (no \
static draws to ship)"
.into(),
);
}
// Place new buffers sized to the rebuilt layout. The outgoing pair is
// replaced below; its ranges return to the pool on drop, withheld until
// the frames the `wait_idle` above drained can no longer reference them.
let new_vertex_buffer = self
.allocator
.alloc_buffer_with_bytes(
bytes_of_slice(new_vertices.as_slice()),
MTLResourceOptions::StorageModeShared,
)
.map_err(|e| format!("rebuild_static_geometry: vertex buffer: {e}"))?;
let new_index_buffer = self
.allocator
.alloc_buffer_with_bytes(
bytes_of_slice(new_indices.as_slice()),
MTLResourceOptions::StorageModeShared,
)
.map_err(|e| format!("rebuild_static_geometry: index buffer: {e}"))?;
// Apply the new per-draw layout.
for (i, (v_off, v_count, i_off, i_count, base_v, lods)) in
new_layouts.into_iter().enumerate()
{
let obj = &mut self.draw.objects[i];
obj.vertex_offset = v_off;
obj.vertex_count = v_count;
obj.index_offset = i_off;
obj.index_count = i_count;
obj.base_vertex = base_v;
obj.lod_alternates = lods;
}
self.vertex_buffer = new_vertex_buffer;
self.index_buffer = new_index_buffer;
// The RT acceleration structure (if any) was built against the OLD
// vertex/index buffers + draw-object offsets. After this swap its static
// BLAS hold the stale geometry and its geometry table carries stale
// offsets, so reflections would trace mismatched data -- and the RT shader
// reads the (possibly smaller) new vertex buffer at old offsets, risking
// an out-of-bounds fetch. Rebuild the BVH from the new geometry now. We
// are past `wait_idle` on the editor-only hot-reload path, so a synchronous
// full rebuild (the same path the init build + `Rebuild` diagnostic use) is
// appropriate. Rebuild regardless of `dynamic_mode` -- even a build-once
// (`Off`) BVH is invalid once its source buffers are replaced. A rebuild
// failure leaves the prior BVH in place (`rebuild_rt_accel` only swaps on
// success) and must NOT fail the geometry reload, which already succeeded.
if self.rt.accel.is_some() {
let albedo_count = self.textures.len();
if let Err(e) = self.rebuild_rt_accel(albedo_count) {
tracing::warn!(
"rebuild_static_geometry: RT BVH rebuild failed, reflections may be stale: {e}"
);
}
}
Ok(())
}
}