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::{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}