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