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}