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