Skip to main content

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}