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