Skip to main content

pebble/wgpu/
instance.rs

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