Skip to main content

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::{resolve_storage_buffer, resolve_uniform_buffer, 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    /// [`build_instance_bind_group`]).
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`.
71pub fn binding_index(entries: &[super::binding::BindingEntry], name: &str) -> Option<u32> {
72    entries.iter().find(|e| e.name == name).map(|e| e.binding)
73}
74
75/// Builds a bind group matching each `(name, resource)` pair in `resolved`
76/// to its `@binding(N)` via `target_entries`. Returns `None` if any name
77/// in `resolved` has no matching entry — the caller (`GPUBindingInstance::upload`)
78/// turns that into a `None` upload result via `?`, which the sync system
79/// retries next tick rather than treating as fatal (see [`Asset::upload`]).
80pub fn build_instance_bind_group(
81    device: &wgpu::Device,
82    layout: &wgpu::BindGroupLayout,
83    target_entries: &[super::binding::BindingEntry],
84    resolved: &[(&'static str, wgpu::BindingResource)],
85) -> Option<wgpu::BindGroup> {
86    let mut entries = Vec::with_capacity(resolved.len());
87    for (name, resource) in resolved {
88        let binding = binding_index(target_entries, *name)?;
89        entries.push(wgpu::BindGroupEntry {
90            binding,
91            resource: resource.clone(),
92        })
93    }
94
95    Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
96        label: None,
97        layout,
98        entries: &entries,
99    }))
100}
101
102/// An instance uploaded to the GPU: a bind group ready to set against its
103/// target `T`'s pipeline, plus any owned uniform/storage buffers (from
104/// [`BindingInstanceEntry::Uniform`]/`Storage`) updatable via
105/// [`update`](Self::update). See the [`GPUMaterialInstance`]/
106/// [`GPUComputeInstance`] aliases for the two concrete instantiations.
107pub struct GPUBindingInstance<T> {
108    pub target: RawAssetHandle,
109    pub bind_group: wgpu::BindGroup,
110    /// Named buffers owned by this instance, used for updates.
111    buffers: Vec<(&'static str, wgpu::Buffer)>,
112    _marker: PhantomData<fn() -> T>,
113}
114
115impl<T> GPUBindingInstance<T> {
116    /// Overwrite the buffer bound under `name` (the same name given in
117    /// [`BindingInstanceDescriptor::params`]) with `data`. Logs a warning
118    /// and does nothing if `name` doesn't match an owned buffer — most
119    /// likely a typo, or `name` refers to a texture/sampler entry rather
120    /// than a `Uniform`/`Storage` one.
121    pub fn update(&self, queue: &wgpu::Queue, name: &str, data: &[u8]) {
122        match self.buffers.iter().find(|(n, _)| *n == name) {
123            Some((_, buf)) => update_buffer(queue, buf, data),
124            None => tracing::warn!(
125                "GPUBindingInstance::update: no bound buffer named '{name}' — check for a typo \
126                 against the entries in this instance's BindingInstanceDescriptor"
127            ),
128        }
129    }
130}
131
132impl<T> Asset<WGPUBackend> for GPUBindingInstance<T>
133where
134    T: BindGroupTarget + 'static + Send + Sync,
135{
136    type Source = BindingInstanceDescriptor<T>;
137    type Deps<'a> = (
138        Res<'a, ProcessedAssets<T>>,
139        Res<'a, ProcessedAssets<super::textures::GPUTexture>>,
140        Res<'a, ProcessedAssets<super::texture_array::GPUTextureArray>>,
141        Res<'a, ProcessedAssets<super::cubemap::GPUCubemap>>,
142        Res<'a, GlobalSamplers>,
143    );
144
145    fn upload<'a>(
146        source: &Self::Source,
147        backend: &WGPUBackend,
148        deps: &Self::Deps<'a>,
149    ) -> Option<Self> {
150        let (targets, textures, texture_arrays, cubemaps, samplers) = deps;
151        let target = targets.get(source.target)?;
152
153        // Two passes: first resolve every binding, deferring uniform/storage
154        // buffers to an index into `owned_buffers` rather than taking a
155        // reference immediately — a `BindingResource` borrowed from a Vec
156        // slot can't coexist with later pushes into that same Vec.
157        enum Pending<'a> {
158            Direct(wgpu::BindingResource<'a>),
159            OwnedBuffer(usize),
160        }
161
162        let mut owned_buffers: Vec<(&'static str, wgpu::Buffer)> = Vec::new();
163        let mut pending: Vec<(&'static str, Pending)> = Vec::new();
164
165        for (name, entry) in &source.params {
166            let resource = match entry {
167                BindingInstanceEntry::Texture(id) => {
168                    Pending::Direct(wgpu::BindingResource::TextureView(&textures.get(*id)?.view))
169                }
170                BindingInstanceEntry::TextureArray(id) => Pending::Direct(
171                    wgpu::BindingResource::TextureView(&texture_arrays.get(*id)?.view),
172                ),
173                BindingInstanceEntry::Cubemap(id) => {
174                    Pending::Direct(wgpu::BindingResource::TextureView(&cubemaps.get(*id)?.view))
175                }
176                BindingInstanceEntry::Sampler(kind) => {
177                    Pending::Direct(wgpu::BindingResource::Sampler(samplers.get(*kind)))
178                }
179                BindingInstanceEntry::Uniform(bytes) => {
180                    let buf = resolve_uniform_buffer(&backend.device, bytes.as_slice().into());
181                    owned_buffers.push((*name, buf));
182                    Pending::OwnedBuffer(owned_buffers.len() - 1)
183                }
184                BindingInstanceEntry::Storage(bytes) => {
185                    let buf = resolve_storage_buffer(&backend.device, bytes.as_slice().into());
186                    owned_buffers.push((*name, buf));
187                    Pending::OwnedBuffer(owned_buffers.len() - 1)
188                }
189            };
190            pending.push((*name, resource));
191        }
192
193        let resolved: Vec<(&'static str, wgpu::BindingResource)> = pending
194            .into_iter()
195            .map(|(name, p)| {
196                let resource = match p {
197                    Pending::Direct(r) => r,
198                    Pending::OwnedBuffer(i) => owned_buffers[i].1.as_entire_binding(),
199                };
200                (name, resource)
201            })
202            .collect();
203
204        let bind_group = build_instance_bind_group(
205            &backend.device,
206            target.bind_group_layout(),
207            target.binding_entries(),
208            &resolved,
209        )?;
210
211        Some(Self {
212            target: source.target,
213            bind_group,
214            buffers: owned_buffers,
215            _marker: PhantomData,
216        })
217    }
218}
219
220/// A material instance uploaded to the GPU — [`GPUBindingInstance`] bound
221/// against a [`GPUMaterial`](super::material::GPUMaterial).
222pub type GPUMaterialInstance = GPUBindingInstance<super::material::GPUMaterial>;
223/// Source data for a [`GPUMaterialInstance`].
224pub type MaterialInstanceDescriptor = BindingInstanceDescriptor<super::material::GPUMaterial>;
225
226/// A compute instance uploaded to the GPU — [`GPUBindingInstance`] bound
227/// against a [`GPUCompute`](super::compute::GPUCompute).
228pub type GPUComputeInstance = GPUBindingInstance<super::compute::GPUCompute>;
229/// Source data for a [`GPUComputeInstance`].
230pub type ComputeInstanceDescriptor = BindingInstanceDescriptor<super::compute::GPUCompute>;
231
232crate::wgpu::plugin_macros::asset_plugin! {
233    /// Registers the [`GPUMaterialInstance`] asset pipeline
234    /// (`Assets<MaterialInstanceDescriptor>` → `ProcessedAssets<GPUMaterialInstance>`).
235    /// Included by [`WGPUPlugin`](super::backend::WGPUPlugin); add directly
236    /// only if you're assembling the `wgpu` module's plugins by hand.
237    MaterialInstancePlugin, GPUMaterialInstance
238}
239
240crate::wgpu::plugin_macros::asset_plugin! {
241    /// Registers the [`GPUComputeInstance`] asset pipeline
242    /// (`Assets<ComputeInstanceDescriptor>` → `ProcessedAssets<GPUComputeInstance>`).
243    /// Included by [`WGPUPlugin`](super::backend::WGPUPlugin); add directly
244    /// only if you're assembling the `wgpu` module's plugins by hand.
245    ComputeInstancePlugin, GPUComputeInstance
246}