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
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
//! Per-in-flight-frame storage for the ray-tracing structures the skinned update
//! rewrites every frame: the deformed-vertex buffer, one BLAS per skinned object,
//! the TLAS, and the instance / geometry-table / build-scratch buffers.
//!
//! The skinned update used to allocate all of those fresh every frame and park
//! the outgoing set in the `RetirePool`. That is correct but it is device-
//! allocator traffic at frame rate. Because the skinned update runs on EVERY
//! frame, its outputs fit the ring rule the upload buffers in `frame_rings.rs`
//! already follow: frame `R` writes slot `R % depth` and is the only frame that
//! binds it, and the frames-in-flight fence guarantees the previous writer of
//! that slot (frame `R - depth`) has retired on the GPU. So a slot's storage can
//! simply be rebuilt in place.
//!
//! The rule does NOT extend to the static `rebuild_tlas` path. A sparsely-moving
//! scene keeps tracing one TLAS across many frames without rebuilding, so that
//! structure is read by frames the fence does not pair with its writer; see the
//! `RetirePool` doc comment. Anything published from a ring slot must therefore
//! be unpublished the moment the skinned path stops running, which is what
//! `RtFrameSlot::release` is for.
//!
//! Sizes are high-water: a slot never shrinks, so a steady scene allocates once
//! and then does nothing.
#![deny(unsafe_op_in_unsafe_fn)]
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{
MTLAccelerationStructure, MTLBuffer, MTLDevice, MTLInstanceAccelerationStructureDescriptor,
MTLPrimitiveAccelerationStructureDescriptor, MTLResource as _, MTLResourceOptions,
};
use concinnity_core::render::error::RenderResult;
use concinnity_core::render::rt_refit::SkinnedRefit;
use super::error::allocation_failed;
use super::frame_rings::grow_to;
type Buffer = Retained<ProtocolObject<dyn MTLBuffer>>;
type Structure = Retained<ProtocolObject<dyn MTLAccelerationStructure>>;
type PrimDesc = Retained<MTLPrimitiveAccelerationStructureDescriptor>;
// Identifies the TLAS descriptor a slot has cached. A descriptor pins the array
// of referenced BLAS, the instance buffer and the instance count; while all
// three are unchanged the same descriptor drives every rebuild (a build re-reads
// the instance buffer's current contents), so it does not have to be rebuilt --
// which is what keeps the per-frame `Vec` of BLAS references off the heap.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(super) struct TlasKey {
// Bumped by the owner whenever the persistent BLAS head changes identity.
pub head_generation: u64,
// Bumped by the slot whenever its own BLAS or instance buffer are replaced.
pub slot_generation: u64,
pub instance_count: usize,
}
// A freshly-allocated set of skinned BLAS for one slot, with the descriptors
// they were sized from and the largest build scratch any of them needs. Built by
// the caller (which owns the descriptor shapes) and handed to the slot to own.
pub(super) struct SkinnedBlasSet {
pub blas: Vec<Structure>,
pub descs: Vec<PrimDesc>,
pub scratch_bytes: usize,
}
// One in-flight frame's storage.
pub(super) struct RtFrameSlot {
deformed: Option<Buffer>,
blas: Vec<Structure>,
descs: Vec<PrimDesc>,
// Build scratch the descriptors above reported when `blas` was allocated.
blas_scratch: usize,
// The shapes `blas` were last built over and the refit run since.
pub refit: SkinnedRefit,
tlas: Option<Structure>,
tlas_size: usize,
tlas_desc: Option<(
TlasKey,
Retained<MTLInstanceAccelerationStructureDescriptor>,
)>,
instances: Option<Buffer>,
geom_table: Option<Buffer>,
scratch: Option<Buffer>,
generation: u64,
}
impl RtFrameSlot {
fn new() -> Self {
Self {
deformed: None,
blas: Vec::new(),
descs: Vec::new(),
blas_scratch: 0,
refit: SkinnedRefit::default(),
tlas: None,
tlas_size: 0,
tlas_desc: None,
instances: None,
geom_table: None,
scratch: None,
generation: 0,
}
}
// Bumped whenever this slot replaces a resource a cached TLAS descriptor
// pins, so the owner's `TlasKey` stops matching and the descriptor rebuilds.
pub(super) fn generation(&self) -> u64 {
self.generation
}
// The deformed (posed) skinned vertex buffer, grown to `bytes`. Shared, not
// Private: it is written by the skin compute pass and then read by both the
// acceleration-structure build and the reflection fragment shader, which run
// in separate command buffers -- a Private buffer in that cross-command-
// buffer pattern was observed to GPU page-fault on the fragment read.
//
// The `bool` is true when the buffer was (re)allocated, which invalidates
// every descriptor built over the old one.
pub(super) fn deformed(
&mut self,
device: &ProtocolObject<dyn MTLDevice>,
bytes: usize,
) -> RenderResult<(Buffer, bool)> {
let have = self.deformed.as_ref().map_or(0, |b| b.length());
let mut fresh = false;
if let Some(cap) = grow_to(have, bytes) {
let buf = device
.newBufferWithLength_options(cap, MTLResourceOptions::StorageModeShared)
.ok_or_else(|| allocation_failed("RT deformed-vertex buffer"))?;
buf.setLabel(Some(&super::pipeline::ns_str("rt_deformed_verts")));
self.deformed = Some(buf);
self.generation = self.generation.wrapping_add(1);
fresh = true;
}
let buf = self
.deformed
.as_ref()
.expect("deformed slot was just ensured")
.clone();
Ok((buf, fresh))
}
// The TLAS instance-descriptor upload buffer, grown to `bytes`.
pub(super) fn instances(
&mut self,
device: &ProtocolObject<dyn MTLDevice>,
bytes: usize,
) -> RenderResult<Buffer> {
let have = self.instances.as_ref().map_or(0, |b| b.length());
if let Some(cap) = grow_to(have, bytes) {
self.instances = Some(shared_buffer(
device,
cap,
"rt_instances",
"RT instance descriptors",
)?);
self.generation = self.generation.wrapping_add(1);
}
Ok(self
.instances
.as_ref()
.expect("instance slot was just ensured")
.clone())
}
// The per-instance geometry table the reflection kernel indexes by
// `instance_id`, grown to `bytes`.
pub(super) fn geom_table(
&mut self,
device: &ProtocolObject<dyn MTLDevice>,
bytes: usize,
) -> RenderResult<Buffer> {
let have = self.geom_table.as_ref().map_or(0, |b| b.length());
if let Some(cap) = grow_to(have, bytes) {
self.geom_table = Some(shared_buffer(
device,
cap,
"rt_geom_table",
"RT geometry table",
)?);
}
Ok(self
.geom_table
.as_ref()
.expect("geometry-table slot was just ensured")
.clone())
}
// Private build / refit scratch, grown to `bytes`. Shared by every build on
// this frame's command buffer: separate encoders serialize, so one buffer
// covers them all.
pub(super) fn scratch(
&mut self,
device: &ProtocolObject<dyn MTLDevice>,
bytes: usize,
) -> RenderResult<Buffer> {
let have = self.scratch.as_ref().map_or(0, |b| b.length());
if let Some(cap) = grow_to(have, bytes) {
let buf = device
.newBufferWithLength_options(cap, MTLResourceOptions::StorageModePrivate)
.ok_or_else(|| allocation_failed("RT scratch buffer"))?;
buf.setLabel(Some(&super::pipeline::ns_str("rt_scratch")));
self.scratch = Some(buf);
}
Ok(self
.scratch
.as_ref()
.expect("scratch slot was just ensured")
.clone())
}
// Replace this slot's skinned BLAS with fresh structures, along with the
// descriptors they were sized from and the build scratch they need. The new
// structures hold no tree, so the refit record resets. The outgoing ones are
// dropped in place: a slot is written only by the frame that owns it, and the
// fence guarantees the previous writer retired.
pub(super) fn set_skinned(&mut self, built: SkinnedBlasSet) {
self.blas = built.blas;
self.descs = built.descs;
self.blas_scratch = built.scratch_bytes;
self.refit.reset();
self.generation = self.generation.wrapping_add(1);
}
// Build scratch the slot's skinned BLAS need, from the sizes their
// descriptors reported when they were allocated.
pub(super) fn blas_scratch(&self) -> usize {
self.blas_scratch
}
pub(super) fn skinned_blas(&self) -> &[Structure] {
&self.blas
}
pub(super) fn skinned_descs(&self) -> &[PrimDesc] {
&self.descs
}
// The top-level structure, (re)allocated when `size` outgrows it. Sizing is
// high-water so an instance count that oscillates does not reallocate.
pub(super) fn tlas(
&mut self,
device: &ProtocolObject<dyn MTLDevice>,
size: usize,
) -> RenderResult<Structure> {
if self.tlas.is_none() || self.tlas_size < size {
let tlas = device
.newAccelerationStructureWithSize(size.max(1))
.ok_or_else(|| allocation_failed("TLAS"))?;
tlas.setLabel(Some(&super::pipeline::ns_str("rt_tlas")));
self.tlas = Some(tlas);
self.tlas_size = size;
}
Ok(self
.tlas
.as_ref()
.expect("TLAS slot was just ensured")
.clone())
}
// The cached TLAS descriptor, if it was built for `key`.
pub(super) fn tlas_desc(
&self,
key: TlasKey,
) -> Option<Retained<MTLInstanceAccelerationStructureDescriptor>> {
self.tlas_desc
.as_ref()
.filter(|(cached, _)| *cached == key)
.map(|(_, desc)| desc.clone())
}
pub(super) fn set_tlas_desc(
&mut self,
key: TlasKey,
desc: Retained<MTLInstanceAccelerationStructureDescriptor>,
) {
self.tlas_desc = Some((key, desc));
}
// Forget the structures built over this slot's deformed buffer. Called when
// the skinned path stops publishing (no skinned object is visible this
// frame): the owner must stop binding this slot's resources at the same
// moment, or a later rewrite of the slot could race a frame that still has
// them bound. The buffers are kept -- only the pose-dependent structures are
// invalid.
pub(super) fn release(&mut self) {
self.refit.reset();
if self.blas.is_empty() {
return;
}
self.blas.clear();
self.descs.clear();
self.blas_scratch = 0;
self.generation = self.generation.wrapping_add(1);
}
}
// One slot per frame in flight.
pub(super) struct RtFrameRing {
slots: Vec<RtFrameSlot>,
}
impl RtFrameRing {
// `depth` is the frames-in-flight count; clamped to >= 1. Every slot starts
// empty and allocates on its first use.
pub(super) fn new(depth: usize) -> Self {
Self {
slots: (0..depth.max(1)).map(|_| RtFrameSlot::new()).collect(),
}
}
pub(super) fn slot(&mut self, ring_slot: usize) -> &mut RtFrameSlot {
let idx = ring_slot % self.slots.len();
&mut self.slots[idx]
}
// Drop the pose-dependent structures in every slot. Used when the skinned
// path stops publishing, so no slot stays reachable through a stale handle.
pub(super) fn release_all(&mut self) {
for slot in &mut self.slots {
slot.release();
}
}
}
fn shared_buffer(
device: &ProtocolObject<dyn MTLDevice>,
bytes: usize,
label: &str,
what: &str,
) -> RenderResult<Buffer> {
let buf = device
.newBufferWithLength_options(bytes, MTLResourceOptions::StorageModeShared)
.ok_or_else(|| allocation_failed(format_args!("buffer for {what}")))?;
buf.setLabel(Some(&super::pipeline::ns_str(label)));
Ok(buf)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tlas_key_separates_head_slot_and_instance_count() {
let base = TlasKey {
head_generation: 1,
slot_generation: 2,
instance_count: 3,
};
assert_eq!(base, base);
assert_ne!(
base,
TlasKey {
head_generation: 2,
..base
}
);
assert_ne!(
base,
TlasKey {
slot_generation: 3,
..base
}
);
assert_ne!(
base,
TlasKey {
instance_count: 4,
..base
}
);
}
}