Skip to main content

pebble/wgpu/
instance.rs

1use std::marker::PhantomData;
2
3use crate::{
4    assets::{
5        handle::Handle,
6        storage::{Assets, ProcessedAssets, RawAssetHandle},
7        upload::Asset,
8    },
9    ecs::system::Res,
10    wgpu::{
11        backend::WGPUBackend,
12        binding::BindGroupTarget,
13        buffer::Buffer,
14        buffers::{BindGroup, BindGroupBuilder, BufferBuilder},
15        flags::BufferUsages,
16        samplers::{GlobalSamplers, SamplerKind},
17    },
18};
19
20/// A concrete resource to bind for one named entry of a [`BindingInstance`],
21/// pushed one at a time via [`BindingInstance::texture`]/[`sampler`](BindingInstance::sampler)/etc.
22/// (or directly via [`BindingInstance::param`] for a dynamically-selected
23/// kind). Its `name` is matched against the target's
24/// [`BindingEntry::name`](super::binding::BindingEntry)s to find the right
25/// `@binding(N)` — so this only needs to say *what* to bind, not *where*.
26#[derive(Clone, PartialEq, Eq, Hash)]
27pub enum BindingInstanceEntry {
28    /// A processed [`GPUTexture`](super::textures::GPUTexture), by its
29    /// source handle.
30    Texture(RawAssetHandle),
31    /// A processed [`GPUTextureArray`](super::texture_array::GPUTextureArray),
32    /// by its source handle.
33    TextureArray(RawAssetHandle),
34    /// A processed [`GPUCubemap`](super::cubemap::GPUCubemap), by its
35    /// source handle.
36    Cubemap(RawAssetHandle),
37    /// A sampler from the global sampler cache.
38    Sampler(SamplerKind),
39    /// Raw bytes uploaded into a uniform buffer owned by this instance —
40    /// updatable later via [`GPUBindingInstance::update`].
41    Uniform(Vec<u8>),
42    /// Same as `Uniform` but for a storage buffer.
43    Storage(Vec<u8>),
44}
45
46/// Source data for a [`GPUBindingInstance<T>`]: which `T` (a
47/// [`GPUMaterial`](super::material::GPUMaterial) or
48/// [`GPUCompute`](super::compute::GPUCompute)) to bind against, and the
49/// concrete resource for each of its named binding entries.
50///
51/// `T` is a marker only — this holds no `T` value, just a
52/// [`RawAssetHandle`] into whichever `ProcessedAssets<T>` store `T` lives
53/// in. See the [`MaterialInstance`]/[`ComputeInstance`] aliases for the two
54/// concrete instantiations. Fields are private — start from
55/// [`BindingInstance::new`] and chain the per-kind binding methods below
56/// (mirroring how [`BindGroupBuilder`] adds one resource per call) rather
57/// than constructing one as a struct literal.
58pub struct BindingInstance<T> {
59    /// Handle to the target `T` (looked up in `ProcessedAssets<T>` at
60    /// upload time).
61    target: RawAssetHandle,
62    /// `(entry name, resource)` pairs — every name must match a named
63    /// binding entry on the target, or upload fails (see
64    /// [`GPUBindingInstance`]'s `Asset::upload` impl).
65    params: Vec<(&'static str, BindingInstanceEntry)>,
66    _marker: PhantomData<fn() -> T>,
67}
68
69impl<T: Asset<WGPUBackend>> BindingInstance<T> {
70    /// Start building an instance targeting `target` — the *source* handle
71    /// for `T` (e.g. a `Handle<Material>` for a [`MaterialInstance`]),
72    /// looked up in `ProcessedAssets<T>` once that target itself has
73    /// finished uploading. Chain the binding methods below to add entries.
74    pub fn new(target: Handle<T::Source>) -> Self {
75        Self { target: target.id, params: Vec::new(), _marker: PhantomData }
76    }
77
78    /// Bind a processed [`GPUTexture`](super::textures::GPUTexture), by its
79    /// source handle, under `name`.
80    pub fn texture(mut self, name: &'static str, handle: Handle<super::textures::Texture>) -> Self {
81        self.params.push((name, BindingInstanceEntry::Texture(handle.id)));
82        self
83    }
84
85    /// Bind a processed [`GPUTextureArray`](super::texture_array::GPUTextureArray),
86    /// by its source handle, under `name`.
87    pub fn texture_array(
88        mut self,
89        name: &'static str,
90        handle: Handle<super::texture_array::TextureArray>,
91    ) -> Self {
92        self.params.push((name, BindingInstanceEntry::TextureArray(handle.id)));
93        self
94    }
95
96    /// Bind a processed [`GPUCubemap`](super::cubemap::GPUCubemap), by its
97    /// source handle, under `name`.
98    pub fn cubemap(mut self, name: &'static str, handle: Handle<super::cubemap::Cubemap>) -> Self {
99        self.params.push((name, BindingInstanceEntry::Cubemap(handle.id)));
100        self
101    }
102
103    /// Bind a sampler from the global sampler cache under `name`.
104    pub fn sampler(mut self, name: &'static str, kind: SamplerKind) -> Self {
105        self.params.push((name, BindingInstanceEntry::Sampler(kind)));
106        self
107    }
108
109    /// Bind raw bytes as a uniform buffer owned by this instance, under
110    /// `name` — updatable later via [`GPUBindingInstance::update`].
111    pub fn uniform(mut self, name: &'static str, data: Vec<u8>) -> Self {
112        self.params.push((name, BindingInstanceEntry::Uniform(data)));
113        self
114    }
115
116    /// Same as [`uniform`](Self::uniform) but as a storage buffer.
117    pub fn storage(mut self, name: &'static str, data: Vec<u8>) -> Self {
118        self.params.push((name, BindingInstanceEntry::Storage(data)));
119        self
120    }
121
122    /// Escape hatch for a dynamically-selected entry kind that doesn't fit
123    /// the typed methods above (building entries in a loop over
124    /// heterogeneous data, say). Prefer [`texture`](Self::texture)/
125    /// [`sampler`](Self::sampler)/etc. when the kind is known statically.
126    pub fn param(mut self, name: &'static str, entry: BindingInstanceEntry) -> Self {
127        self.params.push((name, entry));
128        self
129    }
130
131    /// Logs a WARN for an instance with no bound params at all — it
132    /// wouldn't set anything in its target's bind group, almost always a
133    /// sign the binding calls were forgotten rather than intentional.
134    fn validate(&self) {
135        if self.params.is_empty() {
136            tracing::warn!(
137                "BindingInstance::new(): no params — this instance won't bind anything against \
138                 its target; did you forget to chain .texture(...)/.sampler(...)/etc.?"
139            );
140        }
141    }
142
143    /// Consume the builder and return the finished [`BindingInstance`] value.
144    pub fn build(self) -> Self {
145        self.validate();
146        self
147    }
148
149    /// Consume the builder, insert into `assets` under `name`, and return
150    /// the resulting [`Handle<BindingInstance<T>>`].
151    pub fn build_asset(self, name: &str, assets: &mut Assets<Self>) -> Handle<Self> {
152        self.validate();
153        assets.insert(name, self)
154    }
155}
156
157/// Looks up the `@binding(N)` a target declared under `name`. Returning
158/// `None` for an unmatched name (rather than panicking) is what lets
159/// `GPUBindingInstance::upload` turn a bad name into a `None` upload result
160/// via `?` — the sync system retries next tick rather than treating it as
161/// fatal (see [`Asset::upload`]).
162pub fn binding_index(entries: &[super::binding::BindingEntry], name: &str) -> Option<u32> {
163    entries.iter().find(|e| e.name == name).map(|e| e.binding)
164}
165
166/// An instance uploaded to the GPU: a bind group ready to set against its
167/// target `T`'s pipeline, plus any owned uniform/storage buffers (from
168/// [`BindingInstanceEntry::Uniform`]/`Storage`) updatable via
169/// [`update`](Self::update). See the [`GPUMaterialInstance`]/
170/// [`GPUComputeInstance`] aliases for the two concrete instantiations.
171pub struct GPUBindingInstance<T> {
172    pub target: RawAssetHandle,
173    pub bind_group: BindGroup,
174    /// Named buffers owned by this instance, used for updates.
175    buffers: Vec<(&'static str, Buffer)>,
176    _marker: PhantomData<fn() -> T>,
177}
178
179impl<T> GPUBindingInstance<T> {
180    /// Overwrite the buffer bound under `name` (the same name given to
181    /// [`BindingInstance::uniform`]/[`storage`](BindingInstance::storage))
182    /// with `data`. Logs a warning
183    /// and does nothing if `name` doesn't match an owned buffer — most
184    /// likely a typo, or `name` refers to a texture/sampler entry rather
185    /// than a `Uniform`/`Storage` one.
186    pub fn update(&self, name: &str, data: &[u8]) {
187        match self.buffer(name) {
188            Some(buf) => buf.write(data),
189            None => tracing::warn!(
190                "GPUBindingInstance::update: no bound buffer named '{name}' — check for a typo \
191                 against the entries in this instance's BindingInstance"
192            ),
193        }
194    }
195
196    /// The owned buffer bound under `name` (originally passed to
197    /// [`BindingInstance::uniform`]/[`storage`](BindingInstance::storage)),
198    /// e.g. to
199    /// [`Buffer::read`] a compute pass's result back to the CPU. `None` if
200    /// `name` doesn't match an owned buffer.
201    pub fn buffer(&self, name: &str) -> Option<&Buffer> {
202        self.buffers.iter().find(|(n, _)| *n == name).map(|(_, buf)| buf)
203    }
204}
205
206impl<T> Asset<WGPUBackend> for GPUBindingInstance<T>
207where
208    T: BindGroupTarget + 'static + Send + Sync,
209{
210    type Source = BindingInstance<T>;
211    type Deps<'a> = (
212        Res<'a, ProcessedAssets<T>>,
213        Res<'a, ProcessedAssets<super::textures::GPUTexture>>,
214        Res<'a, ProcessedAssets<super::texture_array::GPUTextureArray>>,
215        Res<'a, ProcessedAssets<super::cubemap::GPUCubemap>>,
216        Res<'a, GlobalSamplers>,
217    );
218
219    fn upload<'a>(
220        source: &Self::Source,
221        backend: &WGPUBackend,
222        deps: &Self::Deps<'a>,
223    ) -> Option<Self> {
224        let (targets, textures, texture_arrays, cubemaps, samplers) = deps;
225        let target = targets.get(source.target)?;
226
227        // Built up front, before assembling the bind group below, so that
228        // pass can borrow from a Vec that's no longer growing — a
229        // `BindGroupBuilder` entry borrowed from a Vec slot can't coexist
230        // with later pushes into that same Vec.
231        let owned_buffers: Vec<(&'static str, Buffer)> = source
232            .params
233            .iter()
234            .filter_map(|(name, entry)| match entry {
235                // `COPY_SRC` in addition to the usual `.uniform()`/`.storage()`
236                // pair — not just `.uniform()`/`.storage()` shorthand — so
237                // `GPUBindingInstance::buffer(name).read()`/`read_as::<T>()`
238                // (documented, real capability: reading a compute result back
239                // to the CPU) actually works instead of failing wgpu's
240                // `COPY_SRC` validation the first time anyone calls it.
241                BindingInstanceEntry::Uniform(bytes) => Some((
242                    *name,
243                    BufferBuilder::new()
244                        .usage(BufferUsages::UNIFORM | BufferUsages::COPY_DST | BufferUsages::COPY_SRC)
245                        .data(bytes)
246                        .build(backend),
247                )),
248                BindingInstanceEntry::Storage(bytes) => Some((
249                    *name,
250                    BufferBuilder::new()
251                        .usage(BufferUsages::STORAGE | BufferUsages::COPY_DST | BufferUsages::COPY_SRC)
252                        .data(bytes)
253                        .build(backend),
254                )),
255                _ => None,
256            })
257            .collect();
258
259        let mut builder = BindGroupBuilder::new(target.bind_group_layout());
260        for (name, entry) in &source.params {
261            let binding = binding_index(target.binding_entries(), name)?;
262            builder = match entry {
263                BindingInstanceEntry::Texture(id) => builder.texture_2d_at(binding, textures.get(*id)?),
264                BindingInstanceEntry::TextureArray(id) => {
265                    builder.texture_array_at(binding, texture_arrays.get(*id)?)
266                }
267                BindingInstanceEntry::Cubemap(id) => builder.texture_cubemap_at(binding, cubemaps.get(*id)?),
268                BindingInstanceEntry::Sampler(kind) => builder.sampler_at(binding, samplers.get(*kind)),
269                BindingInstanceEntry::Uniform(_) | BindingInstanceEntry::Storage(_) => {
270                    let buf = &owned_buffers.iter().find(|(n, _)| n == name)?.1;
271                    builder.buffer_at(binding, buf)
272                }
273            };
274        }
275        let bind_group = builder.build(backend);
276
277        Some(Self {
278            target: source.target,
279            bind_group,
280            buffers: owned_buffers,
281            _marker: PhantomData,
282        })
283    }
284}
285
286/// A material instance uploaded to the GPU — [`GPUBindingInstance`] bound
287/// against a [`GPUMaterial`](super::material::GPUMaterial).
288pub type GPUMaterialInstance = GPUBindingInstance<super::material::GPUMaterial>;
289/// Source data for a [`GPUMaterialInstance`].
290pub type MaterialInstance = BindingInstance<super::material::GPUMaterial>;
291
292/// A compute instance uploaded to the GPU — [`GPUBindingInstance`] bound
293/// against a [`GPUCompute`](super::compute::GPUCompute).
294pub type GPUComputeInstance = GPUBindingInstance<super::compute::GPUCompute>;
295/// Source data for a [`GPUComputeInstance`].
296pub type ComputeInstance = BindingInstance<super::compute::GPUCompute>;
297
298crate::wgpu::plugin_macros::asset_plugin! {
299    /// Registers the [`GPUMaterialInstance`] asset pipeline
300    /// (`Assets<MaterialInstance>` → `ProcessedAssets<GPUMaterialInstance>`).
301    /// Included by [`WGPUPlugin`](super::backend::WGPUPlugin); add directly
302    /// only if you're assembling the `wgpu` module's plugins by hand.
303    MaterialInstancePlugin, GPUMaterialInstance
304}
305
306crate::wgpu::plugin_macros::asset_plugin! {
307    /// Registers the [`GPUComputeInstance`] asset pipeline
308    /// (`Assets<ComputeInstance>` → `ProcessedAssets<GPUComputeInstance>`).
309    /// Included by [`WGPUPlugin`](super::backend::WGPUPlugin); add directly
310    /// only if you're assembling the `wgpu` module's plugins by hand.
311    ComputeInstancePlugin, GPUComputeInstance
312}