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