pebble/wgpu/instance.rs
1use std::marker::PhantomData;
2
3use crate::{
4 assets::{
5 handle::Handle,
6 storage::{Assets, RawAssetHandle},
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(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> 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: target.id, 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.id)));
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.id)));
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.id)));
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: RawAssetHandle,
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(Handle::<T>::new(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(id) => builder.with_texture_2d_at(binding, textures.get(Handle::new(*id))?),
287 BindingInstanceEntry::TextureArray(id) => {
288 builder.with_texture_array_at(binding, texture_arrays.get(Handle::new(*id))?)
289 }
290 BindingInstanceEntry::Cubemap(id) => builder.with_texture_cubemap_at(binding, cubemaps.get(Handle::new(*id))?),
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}