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        buffer::Buffer,
13        buffers::{BindGroup, 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: 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}