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