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