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}