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}