celestialsim/gpu/owned.rs
1//! RAII ownership of Godot `RenderingDevice` RIDs (CEL-91).
2//!
3//! [`Owned<K>`] is the **one** handle type and carries the **only** `Drop` impl for GPU
4//! resources in this crate; [`RidSink`] is the only place a `free_rid` call may live.
5//! `K` is a phantom marker ([`Buffer`], [`Texture`], [`Shader`], [`Pipeline`],
6//! [`UniformSet`]) — Godot frees every kind through the same `free_rid`, so the marker
7//! carries no behaviour, only compile-time separation.
8//!
9//! # DROP-ORDER CONTRACT (read this before adding fields to a GPU-resource struct)
10//!
11//! `RenderingDevice` frees *dependents together with their parent*: freeing a shader or a
12//! buffer also invalidates the uniform sets and pipelines derived from it. Therefore
13//! **uniform sets and pipelines must be freed BEFORE the shaders / buffers / textures they
14//! derive from**, or the later `free_rid` hits an already-dead RID.
15//!
16//! **Nothing in this code enforces that.** It falls out of struct **field declaration
17//! order**, because Rust drops fields top-to-bottom. Consumers (e.g. `ChunkGpuResources`)
18//! must declare, in this order:
19//!
20//! ```ignore
21//! struct ChunkGpuResources {
22//! // 1. uniform sets
23//! set: RdUniformSet,
24//! // 2. pipelines
25//! pipeline: RdPipeline,
26//! // 3. shaders / buffers / textures (the parents)
27//! shader: RdShader,
28//! buffer: RdBuffer,
29//! }
30//! ```
31//!
32//! [`MainDeviceSink`]'s drain is **FIFO**, so the enqueue order produced by that field
33//! order is exactly the order the RIDs are handed to `free_rid`.
34
35use std::marker::PhantomData;
36use std::sync::atomic::AtomicBool;
37#[cfg(not(test))]
38use std::sync::atomic::Ordering;
39use std::sync::{Arc, Mutex, Weak};
40
41use godot::builtin::Rid;
42#[cfg(not(test))]
43use godot::builtin::{Callable, Variant};
44use godot::classes::{MultiMesh, RenderingServer};
45#[allow(unused_imports)]
46use godot::prelude::*;
47
48/// WHO deallocates a RID.
49///
50/// This trait's implementations are the only legal `free_rid` call sites in the crate.
51pub trait RidSink: Send + Sync {
52 /// Release `rid` (possibly deferred — see [`MainDeviceSink`]).
53 fn free(&self, rid: Rid);
54}
55
56/// WHAT kind of RID — a compile-time marker only.
57pub trait RdKind {
58 /// Human-readable kind name, for diagnostics.
59 const LABEL: &'static str;
60}
61
62/// Storage-buffer marker.
63pub struct Buffer;
64/// Texture marker.
65pub struct Texture;
66/// Compute-shader marker.
67pub struct Shader;
68/// Compute-pipeline marker.
69pub struct Pipeline;
70/// Uniform-set marker.
71pub struct UniformSet;
72
73impl RdKind for Buffer {
74 const LABEL: &'static str = "buffer";
75}
76impl RdKind for Texture {
77 const LABEL: &'static str = "texture";
78}
79impl RdKind for Shader {
80 const LABEL: &'static str = "shader";
81}
82impl RdKind for Pipeline {
83 const LABEL: &'static str = "pipeline";
84}
85impl RdKind for UniformSet {
86 const LABEL: &'static str = "uniform_set";
87}
88
89/// The one RAII handle for a `RenderingDevice` RID.
90///
91/// Dropping it (including by *overwriting* it with a new handle) frees the RID through the
92/// sink it was created with. See the module docs for the drop-order contract.
93pub struct Owned<K: RdKind> {
94 rid: Rid,
95 sink: Arc<dyn RidSink>,
96 _kind: PhantomData<K>,
97}
98
99impl<K: RdKind> Owned<K> {
100 /// Take ownership of `rid`, to be released through `sink`.
101 pub fn new(rid: Rid, sink: Arc<dyn RidSink>) -> Self {
102 Self {
103 rid,
104 sink,
105 _kind: PhantomData,
106 }
107 }
108
109 /// A `Rid::Invalid` placeholder for a not-yet-built field. Never freed.
110 pub fn invalid(sink: Arc<dyn RidSink>) -> Self {
111 Self::new(Rid::Invalid, sink)
112 }
113
114 /// The owned RID (still owned by `self` — do not free it).
115 pub fn rid(&self) -> Rid {
116 self.rid
117 }
118
119 /// Whether this handle holds a real (non-`Invalid`) RID.
120 pub fn is_valid(&self) -> bool {
121 self.rid.is_valid()
122 }
123}
124
125impl<K: RdKind> Drop for Owned<K> {
126 fn drop(&mut self) {
127 if self.rid.is_valid() {
128 self.sink.free(self.rid);
129 }
130 }
131}
132
133impl<K: RdKind> std::fmt::Debug for Owned<K> {
134 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135 write!(f, "Owned<{}>({})", K::LABEL, self.rid)
136 }
137}
138
139/// Owned storage buffer.
140pub type RdBuffer = Owned<Buffer>;
141/// Owned texture.
142pub type RdTexture = Owned<Texture>;
143/// Owned compute shader.
144pub type RdShader = Owned<Shader>;
145/// Owned compute pipeline.
146pub type RdPipeline = Owned<Pipeline>;
147/// Owned uniform set.
148pub type RdUniformSet = Owned<UniformSet>;
149
150/// Sink for the **main** `RenderingDevice`.
151///
152/// `free_rid` on the main device is render-thread only, but `Drop` fires on whatever thread
153/// the owner happens to die on. So `free` merely enqueues the RID and, the first time the
154/// queue goes non-empty, schedules exactly **one** drain on the render thread. The drain
155/// releases the queued RIDs in **FIFO** order (see the module drop-order contract).
156pub struct MainDeviceSink {
157 queue: Mutex<Vec<Rid>>,
158 scheduled: AtomicBool,
159 /// Self-reference, so `free(&self)` can hand an owning `Arc` to the render-thread
160 /// callable (which must be `'static`).
161 #[cfg_attr(test, allow(dead_code))]
162 me: Weak<MainDeviceSink>,
163}
164
165impl MainDeviceSink {
166 /// Create a sink. `Arc` because every [`Owned`] handle shares it.
167 pub fn new() -> Arc<Self> {
168 Arc::new_cyclic(|me| Self {
169 queue: Mutex::new(Vec::new()),
170 scheduled: AtomicBool::new(false),
171 me: me.clone(),
172 })
173 }
174
175 /// Free every queued RID, FIFO. **Render thread only.**
176 #[cfg(not(test))]
177 fn drain(&self) {
178 self.scheduled.store(false, Ordering::SeqCst);
179 let rids: Vec<Rid> = std::mem::take(&mut *self.queue.lock().unwrap());
180 if rids.is_empty() {
181 return;
182 }
183 let Some(mut rd) = RenderingServer::singleton().get_rendering_device() else {
184 return;
185 };
186 for rid in rids {
187 rd.free_rid(rid);
188 }
189 }
190
191 /// RIDs queued but not yet drained (tests only — there is no Godot server under
192 /// `cargo test`, so nothing is ever scheduled).
193 #[cfg(test)]
194 pub fn pending(&self) -> Vec<Rid> {
195 self.queue.lock().unwrap().clone()
196 }
197}
198
199impl RidSink for MainDeviceSink {
200 #[cfg(not(test))]
201 fn free(&self, rid: Rid) {
202 {
203 let mut q = self.queue.lock().unwrap();
204 q.push(rid);
205 }
206 // Schedule the drain exactly once per pending batch. The callable keeps the sink
207 // alive until the render thread has run it.
208 if !self.scheduled.swap(true, Ordering::SeqCst) {
209 let Some(sink) = self.me.upgrade() else {
210 self.scheduled.store(false, Ordering::SeqCst);
211 return;
212 };
213 let callable = Callable::from_sync_fn("ces_gpu_free_drain", move |_args| {
214 sink.drain();
215 Variant::nil()
216 });
217 RenderingServer::singleton().call_on_render_thread(&callable);
218 }
219 }
220
221 #[cfg(test)]
222 fn free(&self, rid: Rid) {
223 // No Godot server under `cargo test`: only enqueue.
224 let _ = &self.scheduled;
225 self.queue.lock().unwrap().push(rid);
226 }
227}
228
229/// Sink for a **local** `RenderingDevice` (the GPU parity tests in `gpu/chunk_gpu_test.rs`).
230///
231/// Godot frees a local device's resources when the device itself is freed, and the device
232/// outlives them — so releasing individual RIDs is unnecessary: this is a no-op.
233pub struct LocalDeviceSink;
234
235impl LocalDeviceSink {
236 /// Create a shareable no-op sink.
237 #[allow(dead_code)]
238 pub fn new() -> Arc<Self> {
239 Arc::new(Self)
240 }
241}
242
243impl RidSink for LocalDeviceSink {
244 fn free(&self, _rid: Rid) {}
245}
246
247/// A `MultiMesh` allocated with `use_indirect`, plus the cleanup Godot forgets.
248///
249/// Godot never releases an indirect `MultiMesh`'s internal command buffer when the
250/// multimesh is freed — an engine bug, reproducible with no CelestialSim code at all
251/// (allocate four indirect multimeshes in GDScript, drop them, and Godot reports
252/// `4 RIDs of type "StorageBuffer" were leaked` on exit). Since every planet rebuild
253/// (a builder re-set, a `tile_res` edit, a scatter-layer change) allocates a fresh set,
254/// the leak grows without bound.
255///
256/// So the multimesh is owned through this handle: on drop it releases the command buffer
257/// **before** letting go of the `Gd<MultiMesh>`, which is the free Godot should have done.
258/// If a future Godot fixes the bug, this becomes a double free — it will announce itself
259/// loudly as a "free of invalid RID" error on the next engine bump, which is the failure
260/// mode we want (noisy, not silent).
261pub struct IndirectMultiMesh {
262 mm: Gd<MultiMesh>,
263 sink: Arc<dyn RidSink>,
264}
265
266impl IndirectMultiMesh {
267 /// Take ownership of an already-`multimesh_allocate_data`'d indirect multimesh.
268 pub fn new(mm: Gd<MultiMesh>, sink: Arc<dyn RidSink>) -> Self {
269 Self { mm, sink }
270 }
271}
272
273impl std::ops::Deref for IndirectMultiMesh {
274 type Target = Gd<MultiMesh>;
275 fn deref(&self) -> &Self::Target {
276 &self.mm
277 }
278}
279
280impl Drop for IndirectMultiMesh {
281 fn drop(&mut self) {
282 // Resolve the command buffer HERE rather than at construction: the renderer
283 // creates it lazily and can recreate it, so the RID is only knowable now — while
284 // the multimesh is still alive, which is precisely why this must happen on drop
285 // and not after the `Gd` is released.
286 let cmd = RenderingServer::singleton().multimesh_get_command_buffer_rd_rid(self.mm.get_rid());
287 if cmd.is_valid() {
288 self.sink.free(cmd);
289 }
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296 use godot::builtin::Rid;
297 use std::sync::{Arc, Mutex};
298
299 #[derive(Default)]
300 struct SpySink {
301 freed: Mutex<Vec<Rid>>,
302 }
303
304 impl SpySink {
305 fn new() -> Arc<Self> {
306 Arc::new(Self::default())
307 }
308 fn freed(&self) -> Vec<Rid> {
309 self.freed.lock().unwrap().clone()
310 }
311 }
312
313 impl RidSink for SpySink {
314 fn free(&self, rid: Rid) {
315 self.freed.lock().unwrap().push(rid);
316 }
317 }
318
319 #[test]
320 fn dropping_a_handle_frees_its_rid_through_the_sink() {
321 let spy = SpySink::new();
322 {
323 let _buf: RdBuffer = Owned::new(Rid::new(7), spy.clone());
324 assert!(spy.freed().is_empty());
325 }
326 assert_eq!(spy.freed(), vec![Rid::new(7)]);
327 }
328
329 #[test]
330 fn an_invalid_rid_is_never_freed() {
331 let spy = SpySink::new();
332 {
333 let h: RdTexture = Owned::invalid(spy.clone());
334 assert!(!h.is_valid());
335 assert_eq!(h.rid(), Rid::Invalid);
336 }
337 assert!(spy.freed().is_empty());
338 }
339
340 #[test]
341 fn field_order_frees_dependents_before_parents() {
342 // Fields drop top-to-bottom: the uniform set (a dependent) before its shader.
343 #[allow(dead_code)]
344 struct Res {
345 set: RdUniformSet,
346 shader: RdShader,
347 }
348 let spy = SpySink::new();
349 {
350 let _r = Res {
351 set: Owned::new(Rid::new(1), spy.clone()),
352 shader: Owned::new(Rid::new(2), spy.clone()),
353 };
354 }
355 assert_eq!(spy.freed(), vec![Rid::new(1), Rid::new(2)]);
356 }
357
358 #[test]
359 fn overwriting_a_handle_frees_the_old_rid() {
360 let spy = SpySink::new();
361 let mut set: RdUniformSet = Owned::new(Rid::new(10), spy.clone());
362 assert_eq!(set.rid(), Rid::new(10));
363 // Rebinding a live handle drops the old one — the chunk_gpu.rs:598 leak, made
364 // impossible.
365 set = Owned::new(Rid::new(11), spy.clone());
366 assert_eq!(spy.freed(), vec![Rid::new(10)]);
367 assert_eq!(set.rid(), Rid::new(11));
368 drop(set);
369 assert_eq!(spy.freed(), vec![Rid::new(10), Rid::new(11)]);
370 }
371
372 #[test]
373 fn main_device_sink_queues_instead_of_freeing_inline() {
374 let sink = MainDeviceSink::new();
375 {
376 let _p: RdPipeline = Owned::new(Rid::new(42), sink.clone());
377 }
378 assert_eq!(sink.pending(), vec![Rid::new(42)]);
379 }
380}