Skip to main content

unlit_wgpu/
resources.rs

1//! Dependency-tracked GPU resource graph.
2//!
3//! The graph is the renderer's single source of truth for the wgpu resources a
4//! frame draws with — textures and their views, samplers, buffers and bind
5//! groups — and for the *virtual* nodes that hold no handle at all, which
6//! group or stand in for the resources around them. Pipeline objects (shader
7//! modules, layouts, pipelines) are not tracked: they are immutable once
8//! built, and the render pipelines this crate builds are cached by their
9//! variant rather than rebuilt from dependencies. Resources are plain wgpu
10//! handles — the graph does not wrap them — but it remembers which resource
11//! was built from which, so that derived resources can be rebuilt when their
12//! inputs change:
13//!
14//! * [`ResourceGraph::replace`] swaps a resource and marks every resource
15//!   transitively built from it as *dirty*.
16//! * Dropping the last [`ResHandle`] to a resource makes it collectable.
17//! * [`ResourceGraph::maintain`] collects the resources no id and no dependent
18//!   holds any more, and rebuilds the resources marked dirty.
19//!
20//! Nothing happens at the point of the change: `replace` only flips dirty
21//! flags, and taking or dropping an id only moves a reference count. Both take
22//! effect the next time [`ResourceGraph::maintain`] runs, which is where a
23//! frame calls it exactly once — before the resources are read.
24//!
25//! Updates are therefore lazy and precise: uploading new bytes into an
26//! existing buffer does not dirty anything, reallocating it does, and only the
27//! resources that actually consumed the old handle are affected.
28//!
29//! # Rebuilding
30//!
31//! A resource that can be rebuilt is inserted with a *recipe*:
32//! [`insert`](ResourceGraph::insert) takes an optional
33//! [`Rebuild`] closure that produces a fresh handle out of the graph. A recipe
34//! reads the resources it was built from back out of the graph by id rather
35//! than capturing their handles, so it observes the current state of its
36//! inputs — a buffer that was reallocated or an array that was replaced — at
37//! the moment it runs.
38//!
39//! [`ResourceGraph::maintain`] runs the recipes of the dirty nodes in
40//! dependency order, so a rebuilt resource is always built from
41//! already-rebuilt inputs. A dirty node with no recipe is left dirty: the
42//! caller changed something the graph cannot rebuild on its own.
43//!
44//! # Typed ids
45//!
46//! A [`ResHandle`] is typed by the [`ResourceKind`] it names, and the kind is
47//! the resource itself: [`ResHandle<wgpu::Buffer>`](ResHandle) resolves to a
48//! buffer, [`ResHandle<TextureView>`](ResHandle) to a texture view together
49//! with its format, and [`ResHandle<Virtual>`](ResHandle) to a node that
50//! holds no handle.
51//! [`ResourceGraph::get`] hands that resource back directly, so no caller
52//! matches on which variant a node holds and a texture view cannot be read
53//! where a buffer is wanted.
54//!
55//! The kind is a compile-time claim rather than runtime state: the graph stores
56//! every resource in one [`Resource`] enum, and a typed id is an index that
57//! remembers what it names. [Insertion](ResourceGraph::insert) derives the
58//! kind from the resource it is given and
59//! [`replace`](ResourceGraph::replace) requires the replacement to have the
60//! same kind, so an id never names a resource of another kind. Where the kind
61//! is genuinely unknown at compile time — a node's dependencies, a dirty
62//! node — the [erased](ResHandle::erase) `ResHandle<Resource>` names it
63//! instead.
64//!
65//! # Retention
66//!
67//! An id names a strong reference to its resource: cloning one takes another
68//! reference, dropping one gives it up, and [`ResourceGraph::maintain`]
69//! collects the resource once its last reference is gone. There is no removal
70//! call and no handle that goes stale — a resource lives exactly while some id
71//! to it does, and letting go of the last id is all it takes to free it.
72//!
73//! A resource built from another holds a reference to it too: recording an edge
74//! with [`add_dependency`](ResourceGraph::add_dependency) takes one reference
75//! from the dependency on behalf of the dependent, so a dependency counts its
76//! dependents among the references that keep it alive. A resource that exists
77//! only to feed a consumer therefore lives exactly while some consumer does,
78//! and collecting a consumer releases whatever it was the last holder of in the
79//! same pass. Declaring the same edge twice records it once, and so counts
80//! once.
81//!
82//! Insertion is immediate: `insert` adds the node and returns its id, and
83//! [`add_dependency`](ResourceGraph::add_dependency) records an edge. Nothing
84//! defers, so there is no insertion state to finish and no failure to report —
85//! an edge that would close a cycle is a bug in the caller and panics where it
86//! is declared.
87//!
88//! A [`Virtual`] node commonly serves as an *aggregation root* for a group of
89//! resources: the root is built from every part, so the root is the group's
90//! single lifetime entry point. Dropping the last id to the root releases the
91//! parts the root was the last holder of, and [`ResourceGraph::maintain`]
92//! collects them. A part shared by two roots therefore outlives either one
93//! alone.
94
95use std::sync::{Arc, Weak};
96
97use crate::dag::{Dag, EdgeError, NodeId};
98
99/// A kind of resource that can live in a [`ResourceGraph`], and the resource an
100/// id of that kind resolves to.
101///
102/// Implemented for the wgpu handles the graph stores and for [`Virtual`], the
103/// kind that holds no handle. The implementor is not just a tag: it is the
104/// payload [`ResourceGraph::get`] hands out, so the kind and the resource read
105/// as one thing — a `ResHandle<wgpu::Buffer>` names a buffer, not a mark
106/// saying "buffer".
107///
108/// [`Resource`] itself implements the trait as the *erased* kind: the one that
109/// names a resource whose kind is known only at runtime.
110pub trait ResourceKind: Sized {
111    /// Borrow this kind out of a stored [`Resource`], or `None` when that
112    /// resource is of another kind.
113    fn borrow_from(resource: &Resource) -> Option<&Self>;
114
115    /// Take this kind out of a stored [`Resource`], or `None` when that
116    /// resource is of another kind.
117    fn take_from(resource: Resource) -> Option<Self>;
118
119    /// This value as a stored [`Resource`].
120    fn into_resource(self) -> Resource;
121}
122
123/// The kind of a [virtual](Resource::Virtual) node: a graph citizen holding no
124/// wgpu handle.
125///
126/// [`ResHandle<Virtual>`](ResHandle) names such a node. It is an ordinary
127/// node otherwise — it takes dependencies, dependents and liveness like any
128/// resource — and it is the natural aggregation root for a set of resources
129/// that live and die together (see [Retention](self)).
130#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
131pub struct Virtual;
132
133/// The one [`Virtual`] value, so its [`ResourceKind`] can hand out a borrow.
134static VIRTUAL: Virtual = Virtual;
135
136impl ResourceKind for Virtual {
137    fn borrow_from(resource: &Resource) -> Option<&Self> {
138        matches!(resource, Resource::Virtual).then_some(&VIRTUAL)
139    }
140
141    fn take_from(resource: Resource) -> Option<Self> {
142        matches!(resource, Resource::Virtual).then_some(VIRTUAL)
143    }
144
145    fn into_resource(self) -> Resource {
146        Resource::Virtual
147    }
148}
149
150/// A texture view together with the format it views its texture as.
151///
152/// wgpu exposes no view format, and a view may be created with one besides its
153/// texture's — an sRGB view over a non-sRGB swap-chain image, for example — so
154/// the format a view was created with is tracked next to it rather than
155/// assumed from the texture. This is the kind a
156/// [`ResHandle<TextureView>`](ResHandle) names, and
157/// [`TextureExt::create_view`] builds one from a descriptor, so the format and
158/// the view cannot disagree.
159///
160/// A view wgpu itself handed out states its format explicitly with
161/// [`TextureView::new`]: there is no way to recover it afterwards, which is
162/// exactly why this wrapper exists.
163#[expect(
164    clippy::disallowed_types,
165    reason = "the one place the bare wgpu type is named: every other caller \
166              goes through this wrapper so a view's format is never guessed"
167)]
168#[derive(Clone, Debug)]
169pub struct TextureView {
170    /// The view.
171    view: wgpu::TextureView,
172    /// The format the view was created with.
173    format: wgpu::TextureFormat,
174}
175
176#[expect(
177    clippy::disallowed_types,
178    reason = "the wrapper's own accessors are the boundary the bare type \
179              crosses, and the only place it may"
180)]
181impl TextureView {
182    /// Pair `view` with the `format` it views its texture as.
183    pub fn new(view: wgpu::TextureView, format: wgpu::TextureFormat) -> Self {
184        Self { view, format }
185    }
186
187    /// The view itself, for the wgpu calls that bind it.
188    pub fn view(&self) -> &wgpu::TextureView {
189        &self.view
190    }
191
192    /// The texture the view views.
193    pub fn texture(&self) -> &wgpu::Texture {
194        self.view.texture()
195    }
196
197    /// The format the view was created with.
198    pub fn format(&self) -> wgpu::TextureFormat {
199        self.format
200    }
201}
202
203#[expect(
204    clippy::disallowed_types,
205    reason = "unwrapping the wrapper into the bare type has to name that type"
206)]
207impl From<TextureView> for wgpu::TextureView {
208    fn from(value: TextureView) -> Self {
209        value.view
210    }
211}
212
213/// Two views are equal when they are the same view of the same texture.
214///
215/// Written out rather than derived because the derive expands to a comparison
216/// of the bare [`wgpu::TextureView`] in generated code the struct's own
217/// `#[expect]` does not cover. Going through [`TextureView::view`] and
218/// [`TextureView::format`] keeps the bare type named only in the wrapper, which
219/// is the whole point of it.
220impl PartialEq for TextureView {
221    fn eq(&self, other: &Self) -> bool {
222        self.format == other.format && self.view() == other.view()
223    }
224}
225
226impl Eq for TextureView {}
227
228/// Creating texture views whose format the graph can track.
229///
230/// wgpu's own [`wgpu::Texture::create_view`] returns a bare
231/// [`wgpu::TextureView`], which reports its texture's format rather than the
232/// one it was created with, so a view that reinterprets its texture — an sRGB
233/// view over a non-sRGB swap-chain image — cannot be told apart from a default
234/// one afterwards. This trait's `create_view` reads the format out of the
235/// descriptor and pairs it with the view.
236///
237/// It is deliberately *not* named differently from wgpu's own method: wgpu's
238/// inherent method shadows it, so a call has to name the trait —
239/// `TextureExt::create_view(&texture, &descriptor)` — which makes the choice
240/// between the two visible at the call site.
241pub trait TextureExt {
242    /// Create a view of this texture, recording the format it views it as.
243    ///
244    /// The format is the descriptor's own when it states one, and the
245    /// texture's otherwise — exactly what wgpu resolves the two to.
246    fn create_view(&self, descriptor: &wgpu::TextureViewDescriptor<'_>) -> TextureView;
247}
248
249#[expect(
250    clippy::disallowed_methods,
251    reason = "the one caller of wgpu's own constructor: reading the format out \
252              of the descriptor is what makes the tracking possible"
253)]
254impl TextureExt for wgpu::Texture {
255    fn create_view(&self, descriptor: &wgpu::TextureViewDescriptor<'_>) -> TextureView {
256        let format = descriptor.format.unwrap_or_else(|| self.format());
257        // Named in full: the trait method would otherwise shadow itself.
258        let view = wgpu::Texture::create_view(self, descriptor);
259        TextureView::new(view, format)
260    }
261}
262
263/// A wgpu resource owned by the graph, or a virtual node standing in for one.
264///
265/// This is the stored form, and the way to name a resource whose kind is only
266/// known at runtime: it is the [erased](ResHandle::erase) [`ResHandle`]'s
267/// kind. Variants are thin — each holds the wgpu handle itself — so callers can
268/// keep working with raw wgpu and use the graph purely for bookkeeping.
269#[derive(Clone, Debug)]
270pub enum Resource {
271    /// A buffer (vertex, index, uniform or storage).
272    Buffer(wgpu::Buffer),
273    /// A texture.
274    Texture(wgpu::Texture),
275    /// A texture view, with the format it views its texture as.
276    TextureView(TextureView),
277    /// A sampler.
278    Sampler(wgpu::Sampler),
279    /// A flat array of fixed-size elements, held either in a storage buffer or
280    /// in a texture.
281    ///
282    /// The two are one kind rather than two because they are one thing to a
283    /// caller: which of them an array lives in follows from the device, not
284    /// from what the array holds. See
285    /// [`ArrayHandle`](crate::texel_array::ArrayHandle).
286    Array(crate::texel_array::ArrayHandle),
287    /// A bind group.
288    BindGroup(wgpu::BindGroup),
289    /// A virtual node: no wgpu handle, purely a graph citizen.
290    ///
291    /// It carries no GPU resource of its own: it is what an id over
292    /// [`Virtual`] resolves to, and it exists to group or stand in for other
293    /// nodes. The edges leaving it express ownership or derivation rather than
294    /// handle derivation, which makes it the natural aggregation root for a
295    /// group of resources that live and die together (see [Retention](self)).
296    ///
297    /// It is an ordinary node otherwise: it takes dependencies like any other,
298    /// and replacing it swaps one virtual value for another — a node's kind
299    /// never changes — while marking the nodes built from it dirty.
300    Virtual,
301}
302
303impl Resource {
304    /// The name of this resource's kind.
305    ///
306    /// The stored form erases the kind, so this is how a caller that only has a
307    /// [`Resource`] — a graph-wide read, a debug dump — reports which of the
308    /// stored kinds it is looking at. The name matches the [`ResourceKind`]
309    /// type it stands for: `"Buffer"`, `"TextureView"`, `"Virtual"`, and so
310    /// on.
311    #[must_use]
312    pub fn kind_name(&self) -> &'static str {
313        match self {
314            Self::Buffer(_) => "Buffer",
315            Self::Texture(_) => "Texture",
316            Self::TextureView(_) => "TextureView",
317            Self::Sampler(_) => "Sampler",
318            Self::Array(_) => "Array",
319            Self::BindGroup(_) => "BindGroup",
320            Self::Virtual => "Virtual",
321        }
322    }
323}
324
325impl ResourceKind for Resource {
326    fn borrow_from(resource: &Resource) -> Option<&Self> {
327        Some(resource)
328    }
329
330    fn take_from(resource: Resource) -> Option<Self> {
331        Some(resource)
332    }
333
334    fn into_resource(self) -> Resource {
335        self
336    }
337}
338
339/// Implement [`ResourceKind`] for a kind stored in one [`Resource`] variant.
340///
341/// The kind is what the variant holds, so the kind's type names the variant
342/// too. Every kind the graph stores is one of these: a variant carries nothing
343/// but its kind, and anything a kind needs beyond its handle — a texture view's
344/// format — lives inside the kind itself rather than beside it in the variant.
345macro_rules! stored_kind {
346    ($( $handle:ty => $variant:ident ),+ $(,)?) => {
347        $(
348            impl ResourceKind for $handle {
349                fn borrow_from(resource: &Resource) -> Option<&Self> {
350                    match resource {
351                        Resource::$variant(handle) => Some(handle),
352                        _ => None,
353                    }
354                }
355
356                fn take_from(resource: Resource) -> Option<Self> {
357                    match resource {
358                        Resource::$variant(handle) => Some(handle),
359                        _ => None,
360                    }
361                }
362
363                fn into_resource(self) -> Resource {
364                    Resource::$variant(self)
365                }
366            }
367        )+
368    };
369}
370
371stored_kind! {
372    wgpu::Buffer => Buffer,
373    wgpu::Texture => Texture,
374    TextureView => TextureView,
375    wgpu::Sampler => Sampler,
376    crate::texel_array::ArrayHandle => Array,
377    wgpu::BindGroup => BindGroup,
378}
379
380/// Implement the erasing conversion into [`Resource`] for a kind whose stored
381/// variant carries it as its only payload.
382macro_rules! into_resource {
383    ($( $handle:ty => $variant:ident ),+ $(,)?) => {
384        $(
385            impl From<$handle> for Resource {
386                fn from(value: $handle) -> Self {
387                    Self::$variant(value)
388                }
389            }
390        )+
391    };
392}
393
394into_resource! {
395    wgpu::Buffer => Buffer,
396    wgpu::Texture => Texture,
397    TextureView => TextureView,
398    wgpu::Sampler => Sampler,
399    crate::texel_array::ArrayHandle => Array,
400    wgpu::BindGroup => BindGroup,
401}
402
403/// A resource's identity as a plain value.
404///
405/// The slot index is the low 32 bits and the generation the slot was in the
406/// high 32 — the same layout as `unlit_ecs::Entity`. A [`ResHandle`] carries
407/// one, and it is the form a handle takes when it has to travel somewhere that
408/// cannot hold the typed handle: a report, a transport, a stored argument.
409/// Resolve it back with [`ResourceGraph::resolve`], which reports `None` once
410/// the slot has been recycled.
411///
412/// It is [`Copy`], unlike a [`ResHandle`], because it holds no reference: it
413/// names the resource but does not keep it alive.
414#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
415#[cfg_attr(feature = "reflect", derive(facet::Facet))]
416#[cfg_attr(feature = "reflect", facet(transparent))]
417pub struct ResId(u64);
418
419impl ResId {
420    /// The id as bits.
421    #[must_use]
422    pub const fn to_bits(self) -> u64 {
423        self.0
424    }
425
426    /// Rebuild an id from the bits [`Self::to_bits`] produced.
427    #[must_use]
428    pub const fn from_bits(bits: u64) -> Self {
429        Self(bits)
430    }
431
432    /// The slot index in the low 32 bits.
433    #[must_use]
434    pub const fn index(self) -> usize {
435        self.0 as u32 as usize
436    }
437
438    /// The slot handle this id names.
439    fn node(self) -> NodeId {
440        NodeId::from_bits(self.0)
441    }
442
443    /// The plain id naming `node`.
444    fn from_node(node: NodeId) -> Self {
445        Self(node.to_bits())
446    }
447}
448
449/// Handle to a resource stored in a [`ResourceGraph`], typed by its kind.
450///
451/// `R` is the [`ResourceKind`] the id names, which is also what the graph hands
452/// back for it: a [`ResHandle<wgpu::Buffer>`](ResHandle) resolves to a
453/// buffer, a [`ResHandle<wgpu::TextureView>`](ResHandle) to a view. The
454/// default, [`Resource`], is the *erased* kind — an id to a resource whose kind
455/// is known only at runtime — which is what graph-wide reads like
456/// [`ResourceGraph::dependencies`] hand out.
457///
458/// An id names a strong reference to its resource: cloning one takes another
459/// reference, dropping one gives it up, and
460/// [`ResourceGraph::maintain`] collects the resource once the last reference is
461/// gone. That is what makes an id safe to hold — the resource it names outlives
462/// every id to it, and a caller that drops its last id lets the graph free the
463/// resource without any further call.
464///
465/// An id therefore resolves for as long as the caller holds it: the graph never
466/// collects a resource some id still names. Letting go of the last id is the
467/// only way to give a resource up, and it takes effect at the next
468/// [`ResourceGraph::maintain`].
469///
470/// An id is not [`Copy`]: cloning one adds a reference, which a copy could not
471/// tell apart from a move. Pass one to a graph method by reference.
472pub struct ResHandle<R = Resource> {
473    node: NodeId,
474    /// The strong reference this id carries. Its liveness is the token's own
475    /// reference count.
476    strong: StrongRef,
477    kind: core::marker::PhantomData<fn() -> R>,
478}
479
480impl<R> ResHandle<R> {
481    /// The graph index this id refers to.
482    ///
483    /// Only meaningful while the resource lives, but stable across the
484    /// resource's lifetime: callers can group or sort by it (ordering draws
485    /// so consecutive ones share a bind group, for example) without holding
486    /// a borrow. The index alone does not identify the resource across a
487    /// removal and reuse of its slot; [`ResHandle::id`] does.
488    pub fn index(&self) -> usize {
489        self.node.index()
490    }
491
492    /// The resource's [`ResId`], for an API that stores ids but does not know
493    /// the type — an inspector that reports one, or a transport that carries
494    /// it.
495    ///
496    /// The id names the resource but holds no reference to it, so it is
497    /// [`Copy`]. Resolve it back through the graph with
498    /// [`ResourceGraph::resolve`], which reports `None` once the slot has
499    /// been recycled.
500    #[must_use]
501    pub fn id(&self) -> ResId {
502        ResId::from_node(self.node)
503    }
504
505    /// Forget the kind, yielding an id to the same resource that claims none.
506    ///
507    /// This is how a typed id is handed to an API that works in any kind — the
508    /// erased [`ResHandle<Resource>`](Resource) that
509    /// [`ResourceGraph::dependencies`] speaks in, or a stored field a caller
510    /// only ever passes back to the graph. The kind is the only thing that
511    /// changes: a strong id stays strong through the call, so the erased id
512    /// names the same reference and both ids keep the resource alive.
513    pub fn erase(&self) -> ResHandle {
514        self.shared::<Resource>()
515    }
516}
517
518impl<R> ResHandle<R> {
519    /// A second id to the same resource, taking one more strong reference to it.
520    ///
521    /// The new id may name any kind: the node is what both ids resolve
522    /// through, and the kind only says which accessor fits. Both [`Clone`] and
523    /// [`Self::erase`] go through here so that a new id always counts,
524    /// whichever way it was made.
525    fn shared<K>(&self) -> ResHandle<K> {
526        ResHandle {
527            node: self.node,
528            strong: self.strong.clone(),
529            kind: core::marker::PhantomData,
530        }
531    }
532}
533
534// The trait impls are written out rather than derived: a derived bound would
535// ask `R` to be comparable, hashable or cloneable, none of which a kind has to
536// be. An id is a node handle, and only the handle takes part in any of them.
537impl<R> Clone for ResHandle<R> {
538    fn clone(&self) -> Self {
539        self.shared::<R>()
540    }
541}
542
543impl<R> PartialEq for ResHandle<R> {
544    fn eq(&self, other: &Self) -> bool {
545        self.node == other.node
546    }
547}
548
549impl<R> Eq for ResHandle<R> {}
550
551impl<R> PartialOrd for ResHandle<R> {
552    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
553        Some(self.cmp(other))
554    }
555}
556
557impl<R> Ord for ResHandle<R> {
558    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
559        self.node.cmp(&other.node)
560    }
561}
562
563impl<R> core::hash::Hash for ResHandle<R> {
564    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
565        self.node.hash(state);
566    }
567}
568
569impl<R> core::fmt::Debug for ResHandle<R> {
570    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
571        write!(f, "ResHandle({})", self.node.index())
572    }
573}
574
575/// The strong reference count of one node is the number of [`ResHandle`]s that
576/// name it plus the number of nodes built from it, which is exactly what
577/// [`Arc`] already counts.
578///
579/// Every node holds a [`Weak`] handle to the [`Arc<()>`](Arc) token its ids
580/// carry, so cloning an id clones the token — one more strong reference — and
581/// dropping it releases the token, with no counter of our own to keep. A
582/// dependent holds one more clone on behalf of each dependency it declared, so
583/// each of a resource's dependents counts towards keeping it alive. Cloning the
584/// [`Weak`] in the node takes no reference, so [`ResourceGraph::maintain`] can
585/// read the live count back without keeping the node alive itself.
586///
587/// The token is an [`Arc`] and not an `Rc` because an id may cross to another
588/// thread even though the graph it names does not: an [`Arc`] makes every id
589/// `Send` and `Sync`, so one can be handed to a worker while the graph stays
590/// where it is. The graph itself is not `Send`: it holds wgpu handles and runs
591/// its recipes, so it stays on the one thread that owns the world it sits in.
592type StrongRef = Arc<()>;
593type StrongRefs = Weak<()>;
594
595#[derive(Debug)]
596struct Node {
597    resource: Resource,
598    /// Set when this resource or one of its dependencies was replaced, and
599    /// cleared once [`ResourceGraph::maintain`] has run its recipe.
600    dirty: bool,
601    /// A handle to the strong references this node's ids carry. Its count is
602    /// the node's liveness: [`ResourceGraph::maintain`] collects the node once
603    /// nothing holds it any more. See [Retention](self).
604    strong: StrongRefs,
605    /// The strong references this node takes on the resources it was built
606    /// from, one per declared dependency, so each of a resource's dependents
607    /// counts towards keeping it alive. Dropping the node releases them.
608    held: Vec<StrongRef>,
609    /// How to build the resource again after one of its inputs was replaced,
610    /// or `None` when the graph cannot rebuild it on its own.
611    rebuild: Option<Rebuild>,
612}
613
614/// A recipe that builds a resource out of the graph it lives in.
615///
616/// A node that can be rebuilt is inserted with one —
617/// [`ResourceGraph::insert`] — and [`ResourceGraph::maintain`]
618/// calls it whenever the node is dirty. The recipe reads whatever it was built
619/// from back out of the graph by [`ResHandle`], so it always sees the current
620/// handle of its inputs rather than a copy captured when it was written.
621///
622/// The closure runs while the graph is borrowed immutably, so it can only read
623/// the graph: a recipe returns the handle it built and the graph writes it into
624/// the node.
625#[derive(Clone)]
626pub struct Rebuild(Arc<dyn Fn(&ResourceGraph) -> Resource>);
627
628impl Rebuild {
629    /// Wrap `rebuild` as a reusable recipe.
630    ///
631    /// Only a resource that can be built purely from what the graph holds can
632    /// be rebuilt lazily; the closure captures the device and the layout it
633    /// needs on its own.
634    pub fn new(rebuild: impl Fn(&ResourceGraph) -> Resource + 'static) -> Self {
635        Self(Arc::new(rebuild))
636    }
637
638    /// Build the resource from the graph's current state.
639    ///
640    /// [`ResourceGraph::maintain`] calls this for a dirty node, but a caller
641    /// can also use it to build the first instance eagerly, when it has to hold
642    /// the resource before the next maintain.
643    ///
644    /// # Panics
645    ///
646    /// If the recipe builds a resource of a different kind than `R` names. A
647    /// recipe only ever builds the kind its node stores, so this is a
648    /// programming error.
649    pub fn build<R: ResourceKind>(&self, graph: &ResourceGraph) -> R {
650        ResourceKind::take_from((self.0)(graph))
651            .expect("the recipe builds a resource of the kind its node stores")
652    }
653}
654
655impl core::fmt::Debug for Rebuild {
656    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
657        // The closure has no useful representation, so print the type alone.
658        f.debug_struct("Rebuild").finish_non_exhaustive()
659    }
660}
661
662/// One resource as [`ResourceGraph::nodes`] reports it.
663///
664/// A borrowed view rather than an owned handle: the [`id`](NodeInfo::id) names
665/// the node and can be resolved back with [`ResourceGraph::resolve`], while the
666/// resource itself is borrowed from the graph for as long as the walk runs.
667#[derive(Debug)]
668pub struct NodeInfo<'a> {
669    /// The node's [`ResId`], the identity [`ResHandle::id`] reports.
670    pub id: ResId,
671    /// The stored resource.
672    pub resource: &'a Resource,
673    /// Whether this resource or one of its dependencies was replaced, and so
674    /// whether [`ResourceGraph::maintain`] still has a recipe to run.
675    pub dirty: bool,
676    /// Whether the graph can rebuild this resource from its inputs on its own.
677    pub rebuildable: bool,
678}
679
680/// One resource slot of a [`ResourceGraph`], as plain data.
681///
682/// The serialisable projection of [`NodeInfo`]: where that borrows the
683/// stored resource, this reports the handle and the shape around it, so a
684/// caller that only inspects the graph needs no access to the wgpu handles.
685///
686/// No bare slot index is kept: the [id](ResourceInfo::id) carries it, and the
687/// slot order is already the order [`ResourceGraph::resource_infos`] returns.
688/// Note that the id's high 32 bits are the generation, so sorting by id orders
689/// by generation rather than by slot; [`ResId::index`] is the slot.
690#[derive(Clone, Debug, PartialEq, Eq)]
691#[cfg_attr(feature = "reflect", derive(facet::Facet))]
692pub struct ResourceInfo {
693    /// The resource's [`ResId`]. Resolve it with [`ResourceGraph::resolve`].
694    pub id: ResId,
695    /// The resource kind, as [`Resource::kind_name`] reports it.
696    pub kind: &'static str,
697    /// Whether this resource or one of its dependencies was replaced, so
698    /// [`ResourceGraph::maintain`] still has work to do.
699    pub dirty: bool,
700    /// Whether the graph can rebuild this resource from its inputs on its own.
701    pub rebuildable: bool,
702}
703
704/// How many resources of one kind a [`ResourceGraph`] holds.
705#[derive(Clone, Debug, PartialEq, Eq)]
706#[cfg_attr(feature = "reflect", derive(facet::Facet))]
707pub struct ResourceKindCount {
708    /// The resource kind, as [`Resource::kind_name`] reports it.
709    pub kind: &'static str,
710    /// How many slots hold a resource of this kind.
711    pub count: usize,
712}
713
714/// The shape of a [`ResourceGraph`], as plain data.
715#[derive(Clone, Debug, PartialEq, Eq)]
716#[cfg_attr(feature = "reflect", derive(facet::Facet))]
717pub struct ResourceGraphInfo {
718    /// Occupied slots, that is [`ResourceGraph::len`]; the `kinds` counts
719    /// sum to it.
720    ///
721    /// A resource nothing holds any more is still counted until the next
722    /// [`ResourceGraph::maintain`] collects it. There is no `empty` field:
723    /// `resources == 0` is it.
724    pub resources: usize,
725    /// Slots that are dirty: replaced themselves, or built from something that
726    /// was, so [`ResourceGraph::maintain`] still has a recipe to run.
727    pub dirty: usize,
728    /// Slots carrying a rebuild recipe, which [`ResourceGraph::maintain`] can
729    /// rebuild on its own.
730    pub rebuildable: usize,
731    /// Occupied slots grouped by kind, sorted by kind name so the report is
732    /// stable.
733    pub kinds: Vec<ResourceKindCount>,
734}
735
736/// What one [`ResourceGraph::maintain`] pass changed.
737#[derive(Clone, Copy, Debug, PartialEq, Eq)]
738#[cfg_attr(feature = "reflect", derive(facet::Facet))]
739pub struct ResourceGraphMaintain {
740    /// Occupied slots before the pass, that is [`ResourceGraph::len`].
741    pub before: usize,
742    /// Occupied slots after the pass: what survived the collection and the
743    /// rebuild.
744    pub after: usize,
745}
746
747/// A directed acyclic graph of wgpu resources.
748///
749/// Every edge points from a dependency to a resource built from it, so the
750/// dependents of a node are exactly the nodes reachable from it. The graph
751/// refuses an edge that would close a cycle, which is what lets
752/// [`Self::maintain`] rebuild its dirty nodes in dependency order.
753#[derive(Debug, Default)]
754pub struct ResourceGraph {
755    graph: Dag<Node>,
756}
757
758impl ResourceGraph {
759    /// Create an empty graph.
760    pub fn new() -> Self {
761        Self::default()
762    }
763
764    /// Number of resources in the graph.
765    ///
766    /// A resource no id holds any more is still counted until the next
767    /// [`Self::maintain`] collects it.
768    pub fn len(&self) -> usize {
769        self.graph.len()
770    }
771
772    /// Whether the graph holds no resources.
773    pub fn is_empty(&self) -> bool {
774        self.graph.is_empty()
775    }
776
777    /// The graph's shape as plain data.
778    ///
779    /// Counts what [`Self::len`] counts: a resource nothing holds any more
780    /// is still included until the next [`Self::maintain`] collects it.
781    pub fn info(&self) -> ResourceGraphInfo {
782        let mut kinds: Vec<ResourceKindCount> = Vec::new();
783        let mut dirty = 0;
784        let mut rebuildable = 0;
785        for (_, node) in self.graph.iter() {
786            dirty += usize::from(node.dirty);
787            rebuildable += usize::from(node.rebuild.is_some());
788            let kind = node.resource.kind_name();
789            match kinds.iter_mut().find(|count| count.kind == kind) {
790                Some(count) => count.count += 1,
791                None => kinds.push(ResourceKindCount { kind, count: 1 }),
792            }
793        }
794        kinds.sort_unstable_by_key(|count| count.kind);
795        ResourceGraphInfo {
796            resources: self.graph.len(),
797            dirty,
798            rebuildable,
799            kinds,
800        }
801    }
802
803    /// Every resource slot, in node-slot order, as plain data.
804    ///
805    /// The serialisable counterpart of [`Self::nodes`]: same order and same
806    /// count, but reporting the [id](ResourceInfo::id) rather than borrowing
807    /// the wgpu resource.
808    pub fn resource_infos(&self) -> Vec<ResourceInfo> {
809        self.graph
810            .iter()
811            .map(|(id, node)| ResourceInfo {
812                id: ResId::from_node(id),
813                kind: node.resource.kind_name(),
814                dirty: node.dirty,
815                rebuildable: node.rebuild.is_some(),
816            })
817            .collect()
818    }
819
820    /// Every resource in the graph, in node-slot order.
821    ///
822    /// Each node's [`id`](NodeInfo::id) is stable while the resource lives, so
823    /// a caller can report it, sort by it, and resolve it back with
824    /// [`Self::resolve`]. The walk covers every slot the graph has ever used,
825    /// so it costs the peak node count rather than the current one.
826    ///
827    /// A resource no id holds any more is still listed until the next
828    /// [`Self::maintain`] collects it, exactly as [`Self::len`] counts it.
829    pub fn nodes(&self) -> impl Iterator<Item = NodeInfo<'_>> {
830        self.graph.iter().map(|(id, node)| NodeInfo {
831            id: ResId::from_node(id),
832            resource: &node.resource,
833            dirty: node.dirty,
834            rebuildable: node.rebuild.is_some(),
835        })
836    }
837
838    /// The handle a [`ResId`] names, or `None` when no resource is there or
839    /// nothing holds it any more.
840    ///
841    /// The handle is erased — its kind is only known from the resource — and
842    /// holds one strong reference, so resolving an id keeps the resource alive
843    /// like any other handle does. An id whose slot has been recycled no longer
844    /// resolves, which is what keeps a stale id from naming a later resource; a
845    /// resource nothing holds is on its way out at the next [`Self::maintain`]
846    /// and cannot be named again, so the call reports `None` rather than
847    /// inventing a reference to it.
848    #[must_use]
849    pub fn resolve(&self, id: ResId) -> Option<ResHandle> {
850        let node = id.node();
851        let strong = self.graph.get(node)?.strong.upgrade()?;
852        Some(ResHandle {
853            node,
854            strong,
855            kind: core::marker::PhantomData,
856        })
857    }
858
859    /// Add `resource` and return an id holding one strong reference to it.
860    ///
861    /// The resource lives while any id to it does, and while any node built
862    /// from it does; [`Self::maintain`] collects it once both are gone. So a
863    /// caller that keeps the returned id keeps the resource, and one that drops
864    /// it — without a dependent — gives the resource up.
865    ///
866    /// The kind of the returned id is the resource's own:
867    /// `graph.insert(buffer, None)` yields a buffer id, and
868    /// `insert(Virtual, None)` a [virtual](Virtual) one. The node is added at
869    /// once; declare what it was built from with [`Self::add_dependency`].
870    ///
871    /// Pass a [`Rebuild`] recipe for a derived resource — a bind group, say —
872    /// that [`Self::maintain`] rebuilds once one of its inputs is replaced. The
873    /// recipe reads its inputs back out of the graph by id, so declare them
874    /// with [`Self::add_dependency`] as well: the edges decide when to rebuild,
875    /// and the recipe decides what to build from. `None` is a resource the
876    /// graph cannot build again on its own.
877    pub fn insert<R: ResourceKind>(
878        &mut self,
879        resource: R,
880        rebuild: Option<Rebuild>,
881    ) -> ResHandle<R> {
882        let strong = Arc::new(());
883        let node = self.graph.insert(Node {
884            resource: resource.into_resource(),
885            dirty: false,
886            strong: Arc::downgrade(&strong),
887            held: Vec::new(),
888            rebuild,
889        });
890        ResHandle {
891            node,
892            strong,
893            kind: core::marker::PhantomData,
894        }
895    }
896
897    /// Record that the resource behind `dependent` was built from the one
898    /// behind `dependency`, so replacing or rebuilding the latter reaches the
899    /// former.
900    ///
901    /// The dependency may be of any kind: one bind group is built from buffers,
902    /// a view and a sampler all at once. Call this once per input; declaring
903    /// the same pair twice records one edge, since a node depends on another
904    /// once or not at all.
905    ///
906    /// The dependent holds one strong reference to the dependency on behalf of
907    /// the edge, so a resource counts its dependents among the references that
908    /// keep it alive: a resource nothing else holds lives exactly while
909    /// something is built from it. Declaring the same edge twice records it
910    /// once, and so takes one reference, not two.
911    ///
912    /// An optional input is the caller's to skip — `if let Some(id) = ...`
913    /// around the call, which reads the way the id itself does — rather than
914    /// something this method silently ignores: an absent dependency is a
915    /// decision about the caller's graph, not an edge.
916    ///
917    /// # Panics
918    ///
919    /// Panics if the edge would close a cycle. A cycle means the declared
920    /// dependencies are not a build order at all, so it is refused where it is
921    /// declared rather than corrupting a later walk.
922    pub fn add_dependency<D, R>(&mut self, dependent: &ResHandle<R>, dependency: &ResHandle<D>) {
923        match self.graph.add_edge(dependency.node, dependent.node) {
924            Ok(false) => {}
925            Ok(true) => {
926                let slot = self
927                    .graph
928                    .get_mut(dependent.node)
929                    .expect("the edge was recorded, so the dependent is in the graph");
930                slot.held.push(dependency.strong.clone());
931            }
932            Err(EdgeError::NoSuchNode(_)) => panic!(
933                "add_dependency: {dependent:?} cannot depend on {dependency:?}, \
934                 because one of the two is not in the graph"
935            ),
936            Err(EdgeError::SelfLoop) => panic!(
937                "add_dependency: {dependent:?} cannot depend on itself \
938                 (the dependency {dependency:?} no longer resolves and its slot was reused)"
939            ),
940            Err(EdgeError::Cycle) => panic!(
941                "add_dependency: {dependent:?} cannot depend on {dependency:?}, \
942                 which would close a cycle"
943            ),
944        }
945    }
946
947    /// Borrow the resource behind `id`.
948    ///
949    /// The id says what it names, so this is the buffer, texture, view,
950    /// sampler or bind group itself; the [erased](ResHandle::erase)
951    /// `ResHandle<Resource>` is how a caller reads a resource whose kind it
952    /// does not know. `None` when the id is unknown — nothing else can make it
953    /// fail, since an id only ever names a resource of its own kind.
954    pub fn get<R: ResourceKind>(&self, id: &ResHandle<R>) -> Option<&R> {
955        self.graph
956            .get(id.node)
957            .and_then(|node| R::borrow_from(&node.resource))
958    }
959
960    /// Replace the resource behind `id` and mark it — and every resource
961    /// transitively built from it — dirty.
962    ///
963    /// The replacement has to be of the id's own kind: a node never changes
964    /// kind, so an id that names a buffer cannot come to name a sampler.
965    ///
966    /// Returns the previous handle, or `None` if `id` is unknown.
967    pub fn replace<R: ResourceKind>(
968        &mut self,
969        id: &ResHandle<R>,
970        resource: impl Into<R>,
971    ) -> Option<R> {
972        R::take_from(self.replace_resource(id.node, resource.into().into_resource())?)
973    }
974
975    /// Swap the resource stored at `node`, marking it and its dependents
976    /// dirty, and hand back what was there.
977    fn replace_resource(&mut self, node: NodeId, resource: Resource) -> Option<Resource> {
978        let previous = {
979            let slot = self.graph.get_mut(node)?;
980            slot.dirty = true;
981            core::mem::replace(&mut slot.resource, resource)
982        };
983        self.mark_dependents_dirty(node);
984        Some(previous)
985    }
986
987    /// Bring the graph up to date: collect the resources nothing holds any
988    /// more, and rebuild what is dirty.
989    ///
990    /// Call this once per frame before reading the resources, so that every
991    /// change made since the last call takes effect at a single point rather
992    /// than at each change.
993    ///
994    /// A resource is collected once nothing holds it: no [`ResHandle`] names
995    /// it and no node is built from it. Dropping the last id to a resource — or
996    /// to a node built from it — is what gives the resource up, and this pass
997    /// is where that takes effect. There is no removal call: a resource some id
998    /// or some dependent still holds stays, and one nothing holds goes.
999    ///
1000    /// Collecting a node releases the references it held on its own
1001    /// dependencies, which may leave one of those with nothing holding it
1002    /// either. The sweep therefore repeats until a pass collects nothing, so a
1003    /// chain of resources that only a dropped consumer kept alive goes in one
1004    /// call.
1005    ///
1006    /// The passes run in a fixed order:
1007    ///
1008    /// 1. Every resource nothing holds is collected, repeating until nothing
1009    ///    more is.
1010    /// 2. Every dirty resource with a [recipe](Rebuild) is rebuilt, in
1011    ///    dependency order so that a rebuild sees its already-rebuilt inputs.
1012    ///
1013    /// Collecting before rebuilding means a resource that is about to be
1014    /// collected is never rebuilt, and a dirty node without a recipe stays
1015    /// dirty: the graph cannot build it again by itself.
1016    pub fn maintain(&mut self) {
1017        while self
1018            .graph
1019            .remove_where_drop(|node| node.strong.strong_count() == 0)
1020            > 0
1021        {}
1022        self.rebuild_dirty();
1023    }
1024
1025    /// The immediate dependencies recorded for `id`.
1026    ///
1027    /// A node's inputs may be of any kind, so the ids come back
1028    /// [erased](ResHandle::erase). Each id is a strong reference in its own
1029    /// right: holding one keeps the input alive, exactly as holding any other
1030    /// id does.
1031    pub fn dependencies<R>(&self, id: &ResHandle<R>) -> impl Iterator<Item = ResHandle> + '_ {
1032        self.graph
1033            .dependencies(id.node)
1034            .map(|node| self.id_for(node))
1035    }
1036
1037    /// An id to `node`, taking one more strong reference to it.
1038    ///
1039    /// The caller asks about the dependency of a node it holds an id to, so the
1040    /// dependent's own reference to that dependency is still in place and the
1041    /// upgrade cannot fail.
1042    fn id_for(&self, node: NodeId) -> ResHandle {
1043        let strong = self
1044            .graph
1045            .get(node)
1046            .and_then(|slot| Weak::upgrade(&slot.strong))
1047            .expect("a dependency of a live node is still held by that node");
1048        ResHandle {
1049            node,
1050            strong,
1051            kind: core::marker::PhantomData,
1052        }
1053    }
1054
1055    /// Rebuild every dirty resource that has a recipe, in dependency order.
1056    ///
1057    /// Dependents are visited after their dependencies, so a rebuild observes
1058    /// already-updated inputs.
1059    fn rebuild_dirty(&mut self) {
1060        // The order is fixed up front, so the rebuilds cannot disturb the walk
1061        // they are part of even though a recipe runs between its steps. A
1062        // recipe only reads the graph, so the nodes it names are all still
1063        // there.
1064        for node in self.graph.topological_order() {
1065            let Some(recipe) = self
1066                .graph
1067                .get(node)
1068                .filter(|slot| slot.dirty)
1069                .and_then(|slot| slot.rebuild.clone())
1070            else {
1071                continue;
1072            };
1073            let rebuilt = recipe.build::<Resource>(self);
1074            let Some(slot) = self.graph.get_mut(node) else {
1075                continue;
1076            };
1077            debug_assert!(
1078                core::mem::discriminant(&slot.resource) == core::mem::discriminant(&rebuilt),
1079                "a rebuilt resource must be of the same kind as the one it replaces"
1080            );
1081            slot.resource = rebuilt;
1082            slot.dirty = false;
1083        }
1084    }
1085
1086    /// Mark `node` and every node built from it dirty.
1087    fn mark_dependents_dirty(&mut self, node: NodeId) {
1088        self.graph.for_each_dependent_mut(node, |slot| {
1089            slot.dirty = true;
1090        });
1091    }
1092}
1093
1094#[cfg(test)]
1095mod tests {
1096    use std::cell::RefCell;
1097    use std::rc::Rc;
1098
1099    use super::*;
1100
1101    /// A resource stand-in: the graph logic never inspects the handle, so the
1102    /// tests can use a buffer.
1103    fn buffer(device: &wgpu::Device, label: &str) -> wgpu::Buffer {
1104        device.create_buffer(&wgpu::BufferDescriptor {
1105            label: Some(label),
1106            size: 64,
1107            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1108            mapped_at_creation: false,
1109        })
1110    }
1111
1112    /// A buffer of a distinctive size, so tests can tell handles apart —
1113    /// wgpu handles carry no comparable identity.
1114    fn sized_buffer(device: &wgpu::Device, size: u64) -> wgpu::Buffer {
1115        device.create_buffer(&wgpu::BufferDescriptor {
1116            label: None,
1117            size,
1118            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1119            mapped_at_creation: false,
1120        })
1121    }
1122
1123    fn device() -> wgpu::Device {
1124        crate::util::test::noop_device().0
1125    }
1126
1127    /// Insert a buffer and hand back the id that keeps it alive.
1128    fn buffer_id(graph: &mut ResourceGraph, device: &wgpu::Device, label: &str) -> BufferId {
1129        graph.insert(buffer(device, label), None)
1130    }
1131
1132    /// The id a [`buffer_id`] hands back: a buffer id, spelled out so the
1133    /// tests do not have to infer it.
1134    type BufferId = ResHandle<wgpu::Buffer>;
1135
1136    /// Insert a buffer whose recipe records that it ran, under `name`, and
1137    /// returns a fresh buffer.
1138    ///
1139    /// The names accumulate in `order`, which is how a test observes both which
1140    /// recipes ran and in what order.
1141    fn rebuildable_buffer(
1142        graph: &mut ResourceGraph,
1143        device: &wgpu::Device,
1144        order: &Rc<RefCell<Vec<&'static str>>>,
1145        name: &'static str,
1146    ) -> BufferId {
1147        let order = Rc::clone(order);
1148        let device = device.clone();
1149        graph.insert(
1150            buffer(&device, name),
1151            Some(Rebuild::new(move |_graph| {
1152                order.borrow_mut().push(name);
1153                buffer(&device, "rebuilt").into()
1154            })),
1155        )
1156    }
1157
1158    #[test]
1159    fn maintain_rebuilds_the_dependents_of_a_replaced_resource() {
1160        let device = device();
1161        let mut graph = ResourceGraph::new();
1162        let base = buffer_id(&mut graph, &device, "base");
1163        let order = Rc::new(RefCell::new(Vec::new()));
1164        let middle = rebuildable_buffer(&mut graph, &device, &order, "middle");
1165        graph.add_dependency(&middle, &base);
1166        let leaf = rebuildable_buffer(&mut graph, &device, &order, "leaf");
1167        graph.add_dependency(&leaf, &middle);
1168
1169        graph.replace(&base, buffer(&device, "base2"));
1170        graph.maintain();
1171
1172        // Dependency order: the middle buffer was rebuilt before the leaf.
1173        assert_eq!(*order.borrow(), vec!["middle", "leaf"]);
1174    }
1175
1176    #[test]
1177    fn maintain_rebuilds_only_the_dependents_of_the_replaced_resource() {
1178        let device = device();
1179        let mut graph = ResourceGraph::new();
1180        let a = buffer_id(&mut graph, &device, "a");
1181        let b = buffer_id(&mut graph, &device, "b");
1182        let order = Rc::new(RefCell::new(Vec::new()));
1183        let a_dependent = rebuildable_buffer(&mut graph, &device, &order, "a");
1184        graph.add_dependency(&a_dependent, &a);
1185        let b_dependent = rebuildable_buffer(&mut graph, &device, &order, "b");
1186        graph.add_dependency(&b_dependent, &b);
1187
1188        graph.replace(&a, buffer(&device, "a2"));
1189        graph.maintain();
1190
1191        assert_eq!(*order.borrow(), vec!["a"]);
1192    }
1193
1194    /// A dirty node with no recipe is left alone rather than preventing the
1195    /// rebuild of the nodes built from it.
1196    #[test]
1197    fn maintain_leaves_a_dirty_node_without_a_recipe_dirty() {
1198        let device = device();
1199        let mut graph = ResourceGraph::new();
1200        let base = buffer_id(&mut graph, &device, "base");
1201        let order = Rc::new(RefCell::new(Vec::new()));
1202        let dependent = rebuildable_buffer(&mut graph, &device, &order, "dependent");
1203        graph.add_dependency(&dependent, &base);
1204
1205        graph.replace(&base, buffer(&device, "base2"));
1206        graph.maintain();
1207        graph.maintain();
1208
1209        // The dependent is rebuilt once; the recipe-less base stays dirty and
1210        // is skipped on every later pass.
1211        assert_eq!(*order.borrow(), vec!["dependent"]);
1212    }
1213
1214    /// The old handle is handed back in the id's own kind, so a caller
1215    /// replacing a buffer never sees another kind of resource.
1216    #[test]
1217    fn replace_returns_the_previous_handle() {
1218        let device = device();
1219        let mut graph = ResourceGraph::new();
1220        let id = graph.insert(sized_buffer(&device, 64), None);
1221
1222        let previous = graph.replace(&id, sized_buffer(&device, 128)).unwrap();
1223        assert_eq!(previous.size(), 64);
1224        assert_eq!(graph.get(&id).unwrap().size(), 128);
1225    }
1226
1227    /// A resource nothing names any more is collected by the next `maintain`,
1228    /// with no removal call in between.
1229    #[test]
1230    fn dropping_the_last_id_makes_a_resource_collectable() {
1231        let device = device();
1232        let mut graph = ResourceGraph::new();
1233        let id = buffer_id(&mut graph, &device, "gone");
1234
1235        graph.maintain();
1236        assert!(graph.get(&id).is_some(), "the id still names it");
1237
1238        drop(id);
1239        graph.maintain();
1240        assert!(graph.is_empty(), "the last reference went with the id");
1241    }
1242
1243    /// A resource a node is built from outlives the id it was inserted with:
1244    /// the dependent's edge holds a reference of its own.
1245    #[test]
1246    fn a_dependent_keeps_a_resource_alive_after_its_id_is_dropped() {
1247        let device = device();
1248        let mut graph = ResourceGraph::new();
1249        let part = graph.insert(buffer(&device, "part"), None);
1250        let dependent = graph.insert(buffer(&device, "dependent"), None);
1251        graph.add_dependency(&dependent, &part);
1252
1253        drop(part);
1254        graph.maintain();
1255        assert_eq!(graph.len(), 2, "the dependent still holds the part");
1256    }
1257
1258    /// The case the graph exists for: dropping a consumer frees the resource it
1259    /// was the last holder of, in the same `maintain`, even though no id to
1260    /// that resource was ever dropped.
1261    #[test]
1262    fn collecting_a_consumer_releases_the_dependency_it_was_the_last_holder_of() {
1263        let device = device();
1264        let mut graph = ResourceGraph::new();
1265        let uniform = graph.insert(buffer(&device, "uniform"), None);
1266        let group = graph.insert(buffer(&device, "group"), None);
1267        graph.add_dependency(&group, &uniform);
1268
1269        drop(group);
1270        drop(uniform);
1271        graph.maintain();
1272        assert!(graph.is_empty(), "both went in one pass");
1273    }
1274
1275    /// The chain is followed to a fixpoint: a resource freed only because
1276    /// another was freed in the same pass is collected too.
1277    #[test]
1278    fn a_chain_of_only_consumers_goes_in_one_maintain() {
1279        let device = device();
1280        let mut graph = ResourceGraph::new();
1281        let leaf = graph.insert(buffer(&device, "leaf"), None);
1282        let middle = graph.insert(buffer(&device, "middle"), None);
1283        graph.add_dependency(&middle, &leaf);
1284        let root = graph.insert(buffer(&device, "root"), None);
1285        graph.add_dependency(&root, &middle);
1286
1287        drop(root);
1288        drop(middle);
1289        drop(leaf);
1290        graph.maintain();
1291        assert!(graph.is_empty());
1292    }
1293
1294    /// A resource two nodes are built from outlives either one alone: the
1295    /// second dependent's edge still holds it.
1296    #[test]
1297    fn a_shared_dependency_survives_while_another_dependent_holds_it() {
1298        let device = device();
1299        let mut graph = ResourceGraph::new();
1300        let shared = graph.insert(buffer(&device, "shared"), None);
1301        let first = graph.insert(buffer(&device, "first"), None);
1302        graph.add_dependency(&first, &shared);
1303        let second = graph.insert(buffer(&device, "second"), None);
1304        graph.add_dependency(&second, &shared);
1305
1306        drop(shared);
1307        drop(first);
1308        graph.maintain();
1309        assert!(
1310            graph.get(&second).is_some(),
1311            "the surviving dependent still owns the shared resource"
1312        );
1313        assert_eq!(graph.len(), 2);
1314
1315        drop(second);
1316        graph.maintain();
1317        assert!(graph.is_empty());
1318    }
1319
1320    /// Declaring the same edge twice counts one reference, not two: dropping
1321    /// the dependency's own id once is enough for the dependent to be the only
1322    /// holder.
1323    #[test]
1324    fn a_repeated_edge_counts_one_reference() {
1325        let device = device();
1326        let mut graph = ResourceGraph::new();
1327        let part = graph.insert(buffer(&device, "part"), None);
1328        let dependent = graph.insert(buffer(&device, "dependent"), None);
1329        graph.add_dependency(&dependent, &part);
1330        graph.add_dependency(&dependent, &part);
1331
1332        let held = graph
1333            .graph
1334            .get(part.node)
1335            .expect("the part is in the graph")
1336            .strong
1337            .strong_count();
1338        assert_eq!(held, 2, "the id and the one edge, not the id and two");
1339    }
1340
1341    /// A cycle is refused where it is declared. Without that, a length-2 or
1342    /// longer cycle would make the topological order silently drop the nodes
1343    /// in it, and a walk would stop rebuilding them without saying so.
1344    #[test]
1345    #[should_panic(expected = "would close a cycle")]
1346    fn add_dependency_rejects_a_cycle() {
1347        let device = device();
1348        let mut graph = ResourceGraph::new();
1349        let a = buffer_id(&mut graph, &device, "a");
1350        let b = graph.insert(buffer(&device, "b"), None);
1351        graph.add_dependency(&b, &a);
1352        // `a` already reaches `b`, so the reverse edge would close a cycle.
1353        graph.add_dependency(&a, &b);
1354    }
1355
1356    #[test]
1357    #[should_panic(expected = "cannot depend on itself")]
1358    fn add_dependency_rejects_a_self_loop() {
1359        let device = device();
1360        let mut graph = ResourceGraph::new();
1361        let id = buffer_id(&mut graph, &device, "a");
1362
1363        graph.add_dependency(&id, &id);
1364    }
1365
1366    /// A node is collected once the graph cannot reach it from anything that
1367    /// still holds it, and a node that is still held is not.
1368    #[test]
1369    fn maintain_keeps_a_node_an_id_still_names() {
1370        let device = device();
1371        let mut graph = ResourceGraph::new();
1372        let held = buffer_id(&mut graph, &device, "held");
1373
1374        graph.maintain();
1375        assert!(graph.get(&held).is_some());
1376    }
1377
1378    /// A recipe reads its inputs back out of the graph, so it sees the handles
1379    /// the graph holds when it runs — the replaced ones, not the originals.
1380    #[test]
1381    fn rebuild_observes_the_updated_handles_of_its_dependencies() {
1382        let device = device();
1383        let mut graph = ResourceGraph::new();
1384        let base = buffer_id(&mut graph, &device, "base");
1385        let observed = Rc::new(RefCell::new(Vec::new()));
1386        let seen = Rc::clone(&observed);
1387        let rebuilt = device.clone();
1388        let observed_base = base.clone();
1389        let dependent = graph.insert(
1390            buffer(&device, "dependent"),
1391            Some(Rebuild::new(move |graph| {
1392                let size = graph
1393                    .get(&observed_base)
1394                    .expect("the dependency outlives the rebuild")
1395                    .size();
1396                seen.borrow_mut().push(size);
1397                buffer(&rebuilt, "rebuilt").into()
1398            })),
1399        );
1400        graph.add_dependency(&dependent, &base);
1401
1402        graph.replace(&base, sized_buffer(&device, 128));
1403        graph.maintain();
1404
1405        assert_eq!(*observed.borrow(), vec![128]);
1406    }
1407
1408    #[test]
1409    fn rebuild_visits_a_diamond_in_dependency_order() {
1410        let device = device();
1411        let mut graph = ResourceGraph::new();
1412        //     base
1413        //    /    \
1414        //  left   right
1415        //    \    /
1416        //     join
1417        let order = Rc::new(RefCell::new(Vec::new()));
1418        let base = rebuildable_buffer(&mut graph, &device, &order, "base");
1419        let left = rebuildable_buffer(&mut graph, &device, &order, "left");
1420        graph.add_dependency(&left, &base);
1421        let right = rebuildable_buffer(&mut graph, &device, &order, "right");
1422        graph.add_dependency(&right, &base);
1423        let join = rebuildable_buffer(&mut graph, &device, &order, "join");
1424        graph.add_dependency(&join, &left);
1425        graph.add_dependency(&join, &right);
1426
1427        graph.replace(&base, buffer(&device, "base2"));
1428        graph.maintain();
1429
1430        // The base and both middle nodes are rebuilt before the join, which
1431        // observes both of them.
1432        let order = order.borrow().clone();
1433        assert_eq!(order.len(), 4);
1434        assert_eq!(order[0], "base");
1435        assert_eq!(order[3], "join");
1436    }
1437
1438    // -- kinds -------------------------------------------------------------
1439
1440    /// An id may cross to another thread even though the graph it names may
1441    /// not: the strong reference it carries is an [`Arc`], so an id is `Send`
1442    /// and `Sync` whatever its kind.
1443    #[test]
1444    fn an_id_crosses_threads() {
1445        fn assert_send_sync<T: Send + Sync>() {}
1446        assert_send_sync::<ResHandle>();
1447        assert_send_sync::<ResHandle<wgpu::Buffer>>();
1448    }
1449
1450    /// A typed id resolves to the resource itself, without the caller
1451    /// knowing which variant the node holds.
1452    #[test]
1453    fn a_typed_id_resolves_to_its_own_kind() {
1454        let device = device();
1455        let mut graph = ResourceGraph::new();
1456        let id = graph.insert(sized_buffer(&device, 64), None);
1457
1458        assert_eq!(graph.get(&id).unwrap().size(), 64);
1459        assert!(graph.get(&id.erase()).is_some());
1460    }
1461
1462    /// Erasing an id keeps it pointing at the same node, so the erased id is
1463    /// what a graph-wide read hands back.
1464    #[test]
1465    fn erasing_keeps_the_node() {
1466        let device = device();
1467        let mut graph = ResourceGraph::new();
1468        let id = buffer_id(&mut graph, &device, "a");
1469        let erased = id.erase();
1470
1471        assert_eq!(erased.index(), id.index());
1472        assert!(matches!(graph.get(&erased), Some(Resource::Buffer(_))));
1473        graph.replace(&id, sized_buffer(&device, 128));
1474        let Resource::Buffer(buffer) = graph.get(&erased).expect("the erased id still resolves")
1475        else {
1476            panic!("the node holds a buffer");
1477        };
1478        assert_eq!(
1479            buffer.size(),
1480            128,
1481            "the erased id reads the node's new resource"
1482        );
1483    }
1484
1485    // -- inspection --------------------------------------------------------
1486
1487    /// Every resource is listed with its slot, kind, dirtiness and whether the
1488    /// graph can rebuild it.
1489    #[test]
1490    fn nodes_reports_every_resource_with_its_kind() {
1491        let device = device();
1492        let mut graph = ResourceGraph::new();
1493        let base = graph.insert(buffer(&device, "base"), None);
1494        let rebuilt = device.clone();
1495        let derived = graph.insert(
1496            buffer(&device, "derived"),
1497            Some(Rebuild::new(move |_| buffer(&rebuilt, "rebuilt").into())),
1498        );
1499        graph.add_dependency(&derived, &base);
1500
1501        let nodes: Vec<_> = graph.nodes().collect();
1502        assert_eq!(nodes.len(), 2, "both resources are listed");
1503        assert_eq!(nodes[0].id, base.id());
1504        assert_eq!(nodes[0].resource.kind_name(), "Buffer");
1505        assert!(!nodes[0].dirty);
1506        assert!(!nodes[0].rebuildable);
1507        assert!(nodes[1].rebuildable, "the derived resource has a recipe");
1508    }
1509
1510    /// The graph's summary counts its slots, groups them by kind, and reports
1511    /// how much work maintain still has.
1512    #[test]
1513    fn the_graph_summary_counts_slots_by_kind() {
1514        let device = device();
1515        let mut graph = ResourceGraph::new();
1516        let base = graph.insert(buffer(&device, "base"), None);
1517        let rebuilt = device.clone();
1518        let derived = graph.insert(
1519            buffer(&device, "derived"),
1520            Some(Rebuild::new(move |_| buffer(&rebuilt, "rebuilt").into())),
1521        );
1522        graph.insert(Resource::Virtual, None);
1523        graph.add_dependency(&derived, &base);
1524        graph.replace(&base, sized_buffer(&device, 128));
1525
1526        let info = graph.info();
1527        assert_eq!(info.resources, 3, "three slots are occupied");
1528        assert_eq!(info.dirty, 2, "the base and its dependent are dirty");
1529        assert_eq!(
1530            info.rebuildable, 1,
1531            "only the derived resource has a recipe"
1532        );
1533        assert_eq!(
1534            info.kinds,
1535            [
1536                ResourceKindCount {
1537                    kind: "Buffer",
1538                    count: 2,
1539                },
1540                ResourceKindCount {
1541                    kind: "Virtual",
1542                    count: 1,
1543                },
1544            ],
1545            "kinds are sorted by name",
1546        );
1547        assert_eq!(
1548            info.kinds.iter().map(|kind| kind.count).sum::<usize>(),
1549            info.resources,
1550            "the two count the same slots",
1551        );
1552
1553        graph.maintain();
1554        assert_eq!(
1555            graph.info().dirty,
1556            1,
1557            "the derived resource was rebuilt, but the base has no recipe to clear it",
1558        );
1559    }
1560
1561    /// The plain-data listing mirrors the borrowed one: same slots in the same
1562    /// order, with the handle instead of the resource.
1563    #[test]
1564    fn the_plain_listing_mirrors_the_borrowed_one() {
1565        let device = device();
1566        let mut graph = ResourceGraph::new();
1567        let base = graph.insert(buffer(&device, "base"), None);
1568        graph.insert(Resource::Virtual, None);
1569
1570        let borrowed: Vec<_> = graph.nodes().map(|node| node.id).collect();
1571        let plain: Vec<_> = graph
1572            .resource_infos()
1573            .into_iter()
1574            .map(|info| info.id)
1575            .collect();
1576        assert_eq!(plain, borrowed);
1577        assert_eq!(plain[0], base.id());
1578        assert_eq!(graph.resource_infos()[0].kind, "Buffer");
1579        assert_eq!(graph.resource_infos()[1].kind, "Virtual");
1580    }
1581
1582    /// A resource that is dirty is reported as such until maintain runs its
1583    /// recipe.
1584    #[test]
1585    fn nodes_reports_dirtiness() {
1586        let device = device();
1587        let mut graph = ResourceGraph::new();
1588        let base = graph.insert(buffer(&device, "base"), None);
1589        let rebuilt = device.clone();
1590        let derived = graph.insert(
1591            buffer(&device, "derived"),
1592            Some(Rebuild::new(move |_| buffer(&rebuilt, "rebuilt").into())),
1593        );
1594        graph.add_dependency(&derived, &base);
1595
1596        graph.replace(&base, sized_buffer(&device, 128));
1597        let dirty: Vec<_> = graph.nodes().filter(|node| node.dirty).collect();
1598        assert_eq!(dirty.len(), 2, "the base and its dependent are dirty");
1599
1600        graph.maintain();
1601        let still_dirty: Vec<_> = graph.nodes().filter(|node| node.dirty).collect();
1602        assert_eq!(
1603            still_dirty.len(),
1604            1,
1605            "only the recipe-less node stays dirty"
1606        );
1607        assert_eq!(still_dirty[0].id, base.id());
1608    }
1609
1610    /// The kinds a stored resource can be are named, and the name is the
1611    /// erased kind a graph-wide read reports.
1612    #[test]
1613    fn a_resource_names_its_kind() {
1614        assert_eq!(Resource::Virtual.kind_name(), "Virtual");
1615        let device = device();
1616        let mut graph = ResourceGraph::new();
1617        let id = graph.insert(buffer(&device, "buffer"), None);
1618        let erased = id.erase();
1619        assert_eq!(graph.get(&erased).unwrap().kind_name(), "Buffer");
1620    }
1621
1622    /// A handle resolves back to an id to the same resource.
1623    #[test]
1624    fn a_handle_resolves_back_to_its_resource() {
1625        let device = device();
1626        let mut graph = ResourceGraph::new();
1627        let id = buffer_id(&mut graph, &device, "a");
1628        let plain = id.id();
1629
1630        let resolved = graph.resolve(plain).expect("the slot holds a resource");
1631        assert_eq!(resolved.id(), plain);
1632        assert!(matches!(graph.get(&resolved), Some(Resource::Buffer(_))));
1633        assert_eq!(
1634            graph.resolve(ResId::from_bits(plain.to_bits().wrapping_add(1))),
1635            None,
1636            "the next slot is vacant"
1637        );
1638    }
1639
1640    /// A handle stops resolving once its slot is reused, so a stale handle
1641    /// cannot come to name a later resource.
1642    #[test]
1643    fn a_stale_handle_does_not_resolve_to_the_slot_s_new_resource() {
1644        let device = device();
1645        let mut graph = ResourceGraph::new();
1646        let id = buffer_id(&mut graph, &device, "a");
1647        let plain = id.id();
1648        let index = id.index();
1649        drop(id);
1650        graph.maintain();
1651
1652        let reused = buffer_id(&mut graph, &device, "b");
1653        assert_eq!(reused.index(), index, "the slot is reused");
1654        assert!(
1655            graph.resolve(plain).is_none(),
1656            "the old handle names nothing",
1657        );
1658        assert!(graph.resolve(reused.id()).is_some());
1659    }
1660
1661    /// A handle keeps the resource alive like any other id, so resolving one
1662    /// does not race the next maintain.
1663    #[test]
1664    fn a_handle_resolves_only_while_the_resource_is_held() {
1665        let device = device();
1666        let mut graph = ResourceGraph::new();
1667        let id = buffer_id(&mut graph, &device, "a");
1668        let plain = id.id();
1669        drop(id);
1670
1671        assert!(
1672            graph.resolve(plain).is_none(),
1673            "a resource nothing holds cannot be named again",
1674        );
1675        graph.maintain();
1676        assert!(graph.is_empty());
1677    }
1678
1679    /// A view records the format its descriptor stated.
1680    ///
1681    /// This is the whole point of [`TextureView`] and [`TextureExt`]: wgpu's
1682    /// bare method reports the texture's format for a view that reinterprets
1683    /// it, so the two would otherwise be indistinguishable.
1684    #[test]
1685    fn a_texture_view_records_the_format_it_was_created_with() {
1686        let device = device();
1687        let texture = device.create_texture(&wgpu::TextureDescriptor {
1688            label: None,
1689            size: wgpu::Extent3d {
1690                width: 4,
1691                height: 4,
1692                depth_or_array_layers: 1,
1693            },
1694            mip_level_count: 1,
1695            sample_count: 1,
1696            dimension: wgpu::TextureDimension::D2,
1697            format: wgpu::TextureFormat::Rgba8Unorm,
1698            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1699            // The sRGB view the tests create has to be declared up front,
1700            // which is what a swap chain does for the view it presents.
1701            view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb],
1702        });
1703        let mut graph = ResourceGraph::new();
1704        let default = graph.insert(
1705            TextureExt::create_view(&texture, &wgpu::TextureViewDescriptor::default()),
1706            None,
1707        );
1708        assert_eq!(
1709            graph.get(&default).map(TextureView::format),
1710            Some(wgpu::TextureFormat::Rgba8Unorm)
1711        );
1712
1713        let srgb = graph.insert(
1714            TextureExt::create_view(
1715                &texture,
1716                &wgpu::TextureViewDescriptor {
1717                    format: Some(wgpu::TextureFormat::Rgba8UnormSrgb),
1718                    ..Default::default()
1719                },
1720            ),
1721            None,
1722        );
1723        assert_eq!(
1724            graph.get(&srgb).map(TextureView::format),
1725            Some(wgpu::TextureFormat::Rgba8UnormSrgb)
1726        );
1727        // The view really is an sRGB one over a non-sRGB texture, which wgpu
1728        // itself cannot report back.
1729        assert_eq!(
1730            graph.get(&srgb).unwrap().view().texture().format(),
1731            wgpu::TextureFormat::Rgba8Unorm
1732        );
1733    }
1734
1735    /// Replacing a view replaces its format with it: the two travel together
1736    /// in a [`TextureView`], so they cannot drift apart.
1737    #[test]
1738    fn replace_records_the_new_views_format() {
1739        let device = device();
1740        let texture = device.create_texture(&wgpu::TextureDescriptor {
1741            label: None,
1742            size: wgpu::Extent3d {
1743                width: 4,
1744                height: 4,
1745                depth_or_array_layers: 1,
1746            },
1747            mip_level_count: 1,
1748            sample_count: 1,
1749            dimension: wgpu::TextureDimension::D2,
1750            format: wgpu::TextureFormat::Rgba8Unorm,
1751            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1752            // The sRGB view the tests create has to be declared up front,
1753            // which is what a swap chain does for the view it presents.
1754            view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb],
1755        });
1756        let mut graph = ResourceGraph::new();
1757        let id = graph.insert(
1758            TextureExt::create_view(&texture, &wgpu::TextureViewDescriptor::default()),
1759            None,
1760        );
1761
1762        graph.replace(
1763            &id,
1764            TextureExt::create_view(
1765                &texture,
1766                &wgpu::TextureViewDescriptor {
1767                    format: Some(wgpu::TextureFormat::Rgba8UnormSrgb),
1768                    ..Default::default()
1769                },
1770            ),
1771        );
1772        assert_eq!(
1773            graph.get(&id).map(TextureView::format),
1774            Some(wgpu::TextureFormat::Rgba8UnormSrgb)
1775        );
1776    }
1777
1778    // -- virtual nodes -----------------------------------------------------
1779
1780    /// The aggregation-root pattern: the root is built from its parts, so the
1781    /// root's edge is what keeps each part alive once the caller's own id to it
1782    /// is gone.
1783    #[test]
1784    fn a_virtual_root_keeps_its_parts_alive() {
1785        let device = device();
1786        let mut graph = ResourceGraph::new();
1787        let part = graph.insert(buffer(&device, "part"), None);
1788        let root = graph.insert(Virtual, None);
1789        graph.add_dependency(&root, &part);
1790
1791        drop(part);
1792        graph.maintain();
1793        assert!(graph.get(&root).is_some());
1794        assert_eq!(graph.len(), 2, "the root's edge still holds the part");
1795    }
1796
1797    /// Dropping a virtual root frees the parts it was the last holder of, in
1798    /// the same `maintain`.
1799    #[test]
1800    fn dropping_a_virtual_root_frees_its_parts() {
1801        let device = device();
1802        let mut graph = ResourceGraph::new();
1803        let part = graph.insert(buffer(&device, "part"), None);
1804        let root = graph.insert(Virtual, None);
1805        graph.add_dependency(&root, &part);
1806
1807        drop(part);
1808        drop(root);
1809        graph.maintain();
1810        assert!(graph.is_empty());
1811    }
1812
1813    /// A part two roots are built from outlives either root alone: the other
1814    /// root's edge still holds it.
1815    #[test]
1816    fn a_shared_part_survives_while_another_root_is_alive() {
1817        let device = device();
1818        let mut graph = ResourceGraph::new();
1819        let part = graph.insert(buffer(&device, "part"), None);
1820        let first = graph.insert(Virtual, None);
1821        graph.add_dependency(&first, &part);
1822        let second = graph.insert(Virtual, None);
1823        graph.add_dependency(&second, &part);
1824
1825        drop(part);
1826        drop(first);
1827        graph.maintain();
1828        assert!(
1829            graph.get(&second).is_some(),
1830            "the surviving root still owns the shared part"
1831        );
1832        assert_eq!(graph.len(), 2);
1833
1834        drop(second);
1835        graph.maintain();
1836        assert!(graph.is_empty());
1837    }
1838
1839    /// Replacing a part marks the virtual root built from it dirty, and the
1840    /// root's recipe follows its part in dependency order.
1841    #[test]
1842    fn replacing_a_part_rebuilds_the_virtual_root() {
1843        let device = device();
1844        let mut graph = ResourceGraph::new();
1845        let part = graph.insert(buffer(&device, "part"), None);
1846        let order = Rc::new(RefCell::new(Vec::new()));
1847        let root_order = Rc::clone(&order);
1848        let root = graph.insert(
1849            Virtual,
1850            Some(Rebuild::new(move |_graph| {
1851                root_order.borrow_mut().push("root");
1852                Resource::Virtual
1853            })),
1854        );
1855        graph.add_dependency(&root, &part);
1856
1857        graph.replace(&part, buffer(&device, "part2"));
1858        graph.maintain();
1859        assert_eq!(*order.borrow(), vec!["root"]);
1860    }
1861
1862    /// A virtual node is a graph citizen with no handle: it resolves to
1863    /// [`Virtual`] and to nothing else.
1864    #[test]
1865    fn a_virtual_node_has_no_handle_of_its_own() {
1866        let mut graph = ResourceGraph::new();
1867        let root = graph.insert(Virtual, None);
1868
1869        assert!(matches!(graph.get(&root), Some(Virtual)));
1870    }
1871
1872    /// A virtual node nothing holds is collected like any other node.
1873    #[test]
1874    fn maintain_collects_a_virtual_node_with_no_holders() {
1875        let mut graph = ResourceGraph::new();
1876        let root = graph.insert(Virtual, None);
1877
1878        drop(root);
1879        graph.maintain();
1880        assert!(graph.is_empty());
1881    }
1882
1883    /// A node keeps its kind: replacing a virtual node hands back the previous
1884    /// virtual value, and the node stays a root its parts hang from.
1885    #[test]
1886    fn a_virtual_node_can_be_replaced() {
1887        let device = device();
1888        let mut graph = ResourceGraph::new();
1889        let root = graph.insert(Virtual, None);
1890        let dependent = graph.insert(buffer(&device, "dependent"), None);
1891        graph.add_dependency(&dependent, &root);
1892
1893        let previous = graph
1894            .replace(&root, Virtual)
1895            .expect("the node is in the graph");
1896        assert_eq!(previous, Virtual);
1897        assert!(matches!(graph.get(&root), Some(Virtual)));
1898        assert_eq!(
1899            graph.dependencies(&dependent).collect::<Vec<_>>(),
1900            vec![root.erase()]
1901        );
1902    }
1903}