Skip to main content

pebble/wgpu/
instance.rs

1use std::marker::PhantomData;
2
3use crate::{
4    assets::{
5        storage::{ProcessedAssets, RawAssetHandle},
6        upload::Asset,
7    },
8    ecs::system::Res,
9    wgpu::{
10        backend::WGPUBackend,
11        binding::BindGroupTarget,
12        buffer::Buffer,
13        buffers::{BindGroup, BindGroupBuilder, BufferBuilder},
14        flags::BufferUsages,
15        samplers::{GlobalSamplers, SamplerKind},
16    },
17};
18
19/// A concrete resource to bind for one named entry of a
20/// [`BindingInstanceDescriptor`]. The `name` it's paired with (in
21/// [`BindingInstanceDescriptor::params`]) is matched against the target's
22/// [`BindingEntry::name`](super::binding::BindingEntry)s to find the right
23/// `@binding(N)` — so this only needs to say *what* to bind, not *where*.
24#[derive(Clone, PartialEq, Eq, Hash)]
25pub enum BindingInstanceEntry {
26    /// A processed [`GPUTexture`](super::textures::GPUTexture), by its
27    /// source handle.
28    Texture(RawAssetHandle),
29    /// A processed [`GPUTextureArray`](super::texture_array::GPUTextureArray),
30    /// by its source handle.
31    TextureArray(RawAssetHandle),
32    /// A processed [`GPUCubemap`](super::cubemap::GPUCubemap), by its
33    /// source handle.
34    Cubemap(RawAssetHandle),
35    /// A sampler from the global sampler cache.
36    Sampler(SamplerKind),
37    /// Raw bytes uploaded into a uniform buffer owned by this instance —
38    /// updatable later via [`GPUBindingInstance::update`].
39    Uniform(Vec<u8>),
40    /// Same as `Uniform` but for a storage buffer.
41    Storage(Vec<u8>),
42}
43
44/// Source data for a [`GPUBindingInstance<T>`]: which `T` (a
45/// [`GPUMaterial`](super::material::GPUMaterial) or
46/// [`GPUCompute`](super::compute::GPUCompute)) to bind against, and the
47/// concrete resource for each of its named binding entries.
48///
49/// `T` is a marker only — this holds no `T` value, just a
50/// [`RawAssetHandle`] into whichever `ProcessedAssets<T>` store `T` lives
51/// in. See the [`MaterialInstanceDescriptor`]/[`ComputeInstanceDescriptor`]
52/// aliases for the two concrete instantiations.
53pub struct BindingInstanceDescriptor<T> {
54    /// Handle to the target `T` (looked up in `ProcessedAssets<T>` at
55    /// upload time).
56    pub target: RawAssetHandle,
57    /// `(entry name, resource)` pairs — every name must match a named
58    /// binding entry on the target, or upload fails (see
59    /// [`GPUBindingInstance`]'s `Asset::upload` impl).
60    pub params: Vec<(&'static str, BindingInstanceEntry)>,
61    _marker: PhantomData<fn() -> T>,
62}
63
64// Manual `Default`/construction helper — `#[derive(Default)]` would
65// require `T: Default`, which no target type here needs to satisfy.
66impl<T> BindingInstanceDescriptor<T> {
67    pub fn new(target: RawAssetHandle, params: Vec<(&'static str, BindingInstanceEntry)>) -> Self {
68        Self { target, params, _marker: PhantomData }
69    }
70}
71
72/// Looks up the `@binding(N)` a target declared under `name`. Returning
73/// `None` for an unmatched name (rather than panicking) is what lets
74/// `GPUBindingInstance::upload` turn a bad name into a `None` upload result
75/// via `?` — the sync system retries next tick rather than treating it as
76/// fatal (see [`Asset::upload`]).
77pub fn binding_index(entries: &[super::binding::BindingEntry], name: &str) -> Option<u32> {
78    entries.iter().find(|e| e.name == name).map(|e| e.binding)
79}
80
81/// An instance uploaded to the GPU: a bind group ready to set against its
82/// target `T`'s pipeline, plus any owned uniform/storage buffers (from
83/// [`BindingInstanceEntry::Uniform`]/`Storage`) updatable via
84/// [`update`](Self::update). See the [`GPUMaterialInstance`]/
85/// [`GPUComputeInstance`] aliases for the two concrete instantiations.
86pub struct GPUBindingInstance<T> {
87    pub target: RawAssetHandle,
88    pub bind_group: BindGroup,
89    /// Named buffers owned by this instance, used for updates.
90    buffers: Vec<(&'static str, Buffer)>,
91    _marker: PhantomData<fn() -> T>,
92}
93
94impl<T> GPUBindingInstance<T> {
95    /// Overwrite the buffer bound under `name` (the same name given in
96    /// [`BindingInstanceDescriptor::params`]) with `data`. Logs a warning
97    /// and does nothing if `name` doesn't match an owned buffer — most
98    /// likely a typo, or `name` refers to a texture/sampler entry rather
99    /// than a `Uniform`/`Storage` one.
100    pub fn update(&self, name: &str, data: &[u8]) {
101        match self.buffer(name) {
102            Some(buf) => buf.write(data),
103            None => tracing::warn!(
104                "GPUBindingInstance::update: no bound buffer named '{name}' — check for a typo \
105                 against the entries in this instance's BindingInstanceDescriptor"
106            ),
107        }
108    }
109
110    /// The owned buffer bound under `name` (a `Uniform`/`Storage` entry in
111    /// the original [`BindingInstanceDescriptor::params`]), e.g. to
112    /// [`Buffer::read`] a compute pass's result back to the CPU. `None` if
113    /// `name` doesn't match an owned buffer.
114    pub fn buffer(&self, name: &str) -> Option<&Buffer> {
115        self.buffers.iter().find(|(n, _)| *n == name).map(|(_, buf)| buf)
116    }
117}
118
119impl<T> Asset<WGPUBackend> for GPUBindingInstance<T>
120where
121    T: BindGroupTarget + 'static + Send + Sync,
122{
123    type Source = BindingInstanceDescriptor<T>;
124    type Deps<'a> = (
125        Res<'a, ProcessedAssets<T>>,
126        Res<'a, ProcessedAssets<super::textures::GPUTexture>>,
127        Res<'a, ProcessedAssets<super::texture_array::GPUTextureArray>>,
128        Res<'a, ProcessedAssets<super::cubemap::GPUCubemap>>,
129        Res<'a, GlobalSamplers>,
130    );
131
132    fn upload<'a>(
133        source: &Self::Source,
134        backend: &WGPUBackend,
135        deps: &Self::Deps<'a>,
136    ) -> Option<Self> {
137        let (targets, textures, texture_arrays, cubemaps, samplers) = deps;
138        let target = targets.get(source.target)?;
139
140        // Built up front, before assembling the bind group below, so that
141        // pass can borrow from a Vec that's no longer growing — a
142        // `BindGroupBuilder` entry borrowed from a Vec slot can't coexist
143        // with later pushes into that same Vec.
144        let owned_buffers: Vec<(&'static str, Buffer)> = source
145            .params
146            .iter()
147            .filter_map(|(name, entry)| match entry {
148                // `COPY_SRC` in addition to the usual `.uniform()`/`.storage()`
149                // pair — not just `.uniform()`/`.storage()` shorthand — so
150                // `GPUBindingInstance::buffer(name).read()`/`read_as::<T>()`
151                // (documented, real capability: reading a compute result back
152                // to the CPU) actually works instead of failing wgpu's
153                // `COPY_SRC` validation the first time anyone calls it.
154                BindingInstanceEntry::Uniform(bytes) => Some((
155                    *name,
156                    BufferBuilder::new()
157                        .usage(BufferUsages::UNIFORM | BufferUsages::COPY_DST | BufferUsages::COPY_SRC)
158                        .data(bytes)
159                        .build(backend),
160                )),
161                BindingInstanceEntry::Storage(bytes) => Some((
162                    *name,
163                    BufferBuilder::new()
164                        .usage(BufferUsages::STORAGE | BufferUsages::COPY_DST | BufferUsages::COPY_SRC)
165                        .data(bytes)
166                        .build(backend),
167                )),
168                _ => None,
169            })
170            .collect();
171
172        let mut builder = BindGroupBuilder::new(target.bind_group_layout());
173        for (name, entry) in &source.params {
174            let binding = binding_index(target.binding_entries(), name)?;
175            builder = match entry {
176                BindingInstanceEntry::Texture(id) => builder.texture_2d_at(binding, textures.get(*id)?),
177                BindingInstanceEntry::TextureArray(id) => {
178                    builder.texture_array_at(binding, texture_arrays.get(*id)?)
179                }
180                BindingInstanceEntry::Cubemap(id) => builder.texture_cubemap_at(binding, cubemaps.get(*id)?),
181                BindingInstanceEntry::Sampler(kind) => builder.sampler_at(binding, samplers.get(*kind)),
182                BindingInstanceEntry::Uniform(_) | BindingInstanceEntry::Storage(_) => {
183                    let buf = &owned_buffers.iter().find(|(n, _)| n == name)?.1;
184                    builder.buffer_at(binding, buf)
185                }
186            };
187        }
188        let bind_group = builder.build(backend);
189
190        Some(Self {
191            target: source.target,
192            bind_group,
193            buffers: owned_buffers,
194            _marker: PhantomData,
195        })
196    }
197}
198
199/// A material instance uploaded to the GPU — [`GPUBindingInstance`] bound
200/// against a [`GPUMaterial`](super::material::GPUMaterial).
201pub type GPUMaterialInstance = GPUBindingInstance<super::material::GPUMaterial>;
202/// Source data for a [`GPUMaterialInstance`].
203pub type MaterialInstanceDescriptor = BindingInstanceDescriptor<super::material::GPUMaterial>;
204
205/// A compute instance uploaded to the GPU — [`GPUBindingInstance`] bound
206/// against a [`GPUCompute`](super::compute::GPUCompute).
207pub type GPUComputeInstance = GPUBindingInstance<super::compute::GPUCompute>;
208/// Source data for a [`GPUComputeInstance`].
209pub type ComputeInstanceDescriptor = BindingInstanceDescriptor<super::compute::GPUCompute>;
210
211crate::wgpu::plugin_macros::asset_plugin! {
212    /// Registers the [`GPUMaterialInstance`] asset pipeline
213    /// (`Assets<MaterialInstanceDescriptor>` → `ProcessedAssets<GPUMaterialInstance>`).
214    /// Included by [`WGPUPlugin`](super::backend::WGPUPlugin); add directly
215    /// only if you're assembling the `wgpu` module's plugins by hand.
216    MaterialInstancePlugin, GPUMaterialInstance
217}
218
219crate::wgpu::plugin_macros::asset_plugin! {
220    /// Registers the [`GPUComputeInstance`] asset pipeline
221    /// (`Assets<ComputeInstanceDescriptor>` → `ProcessedAssets<GPUComputeInstance>`).
222    /// Included by [`WGPUPlugin`](super::backend::WGPUPlugin); add directly
223    /// only if you're assembling the `wgpu` module's plugins by hand.
224    ComputeInstancePlugin, GPUComputeInstance
225}