Skip to main content

pebble/graphics/pipeline/
instance.rs

1use std::marker::PhantomData;
2
3use crate::{
4    assets::{handle::Handle, storage::Assets, upload::{Asset, AssetSource}},
5    ecs::resources::Read,
6    graphics::{
7        pipeline::{
8            binding::{BindGroupTarget, BindingEntry},
9            buffers::{BindGroup, BindGroupBuilder, Buffer, BufferBuilder, DynamicBuffer},
10            compute::Compute,
11            cubemap::Cubemap,
12            material::Material,
13            samplers::{GlobalSamplers, SamplerKind},
14            texture_array::TextureArray,
15            textures::Texture,
16        },
17        render::Backend,
18        types::flags::BufferUsages,
19    },
20};
21
22/// One bound value in a [`BindingInstance`] — matched to its bind group slot
23/// by name at upload time.
24#[derive(Clone)]
25pub enum BindingInstanceEntry {
26    Texture(Handle<Texture>),
27    TextureArray(Handle<TextureArray>),
28    Cubemap(Handle<Cubemap>),
29    Sampler(SamplerKind),
30    Uniform(Vec<u8>),
31    Storage(Vec<u8>),
32    Buffer(Buffer),
33    DynamicBuffer(DynamicBuffer),
34}
35
36/// A bind group asset for a [`Material`]/[`Compute`] target — named
37/// textures/samplers/uniforms/storage buffers, matched to the target's
38/// declared entries by name. Usually used via its aliases
39/// [`MaterialInstance`]/[`ComputeInstance`].
40pub struct BindingInstance<T> {
41    target: Handle<T>,
42    params: Vec<(&'static str, BindingInstanceEntry)>,
43    _marker: PhantomData<fn() -> T>,
44}
45
46impl<T> BindingInstance<T>
47where
48    T: Asset<Backend>,
49    T::Processed: BindGroupTarget,
50{
51    pub fn new(target: Handle<T>) -> Self {
52        Self { target, params: Vec::new(), _marker: PhantomData }
53    }
54
55    pub fn with_texture(mut self, name: &'static str, handle: Handle<Texture>) -> Self {
56        self.params.push((name, BindingInstanceEntry::Texture(handle)));
57        self
58    }
59
60    pub fn with_texture_array(mut self, name: &'static str, handle: Handle<TextureArray>) -> Self {
61        self.params.push((name, BindingInstanceEntry::TextureArray(handle)));
62        self
63    }
64
65    pub fn with_cubemap(mut self, name: &'static str, handle: Handle<Cubemap>) -> Self {
66        self.params.push((name, BindingInstanceEntry::Cubemap(handle)));
67        self
68    }
69
70    pub fn with_sampler(mut self, name: &'static str, kind: SamplerKind) -> Self {
71        self.params.push((name, BindingInstanceEntry::Sampler(kind)));
72        self
73    }
74
75    pub fn with_uniform(mut self, name: &'static str, data: Vec<u8>) -> Self {
76        self.params.push((name, BindingInstanceEntry::Uniform(data)));
77        self
78    }
79
80    pub fn with_storage(mut self, name: &'static str, data: Vec<u8>) -> Self {
81        self.params.push((name, BindingInstanceEntry::Storage(data)));
82        self
83    }
84
85    /// Binds an existing [`Buffer`] instead of uploading raw bytes — for a
86    /// buffer you already built yourself (e.g. one a compute pass writes
87    /// to, then another pass reads from). Unlike `.with_uniform`/`.with_storage`,
88    /// no buffer is created here; `buffer` must already carry the usage
89    /// flags this binding needs (`BufferUsages::UNIFORM` or `::STORAGE`,
90    /// matching how the target's own entry for `name` was declared).
91    pub fn with_buffer(mut self, name: &'static str, buffer: Buffer) -> Self {
92        self.params.push((name, BindingInstanceEntry::Buffer(buffer)));
93        self
94    }
95
96    /// Binds an existing [`DynamicBuffer`] — the dynamic-offset counterpart
97    /// to `.with_buffer`. The target's own entry for `name` must have been
98    /// declared with `BindingKind::dynamic_uniform_buffer`/`dynamic_storage_buffer`
99    /// (`has_dynamic_offset: true`) to match, or bind group creation fails
100    /// validation.
101    pub fn with_dynamic_buffer(mut self, name: &'static str, buffer: DynamicBuffer) -> Self {
102        self.params.push((name, BindingInstanceEntry::DynamicBuffer(buffer)));
103        self
104    }
105
106    pub fn with_param(mut self, name: &'static str, entry: BindingInstanceEntry) -> Self {
107        self.params.push((name, entry));
108        self
109    }
110
111    fn validate(&self) {
112        if self.params.is_empty() {
113            tracing::warn!(
114                "BindingInstance::new(): no params — this instance won't bind anything \
115                 against its target; did you forget to chain .with_texture(...)/.with_sampler(...)/etc.?"
116            );
117        }
118    }
119
120    pub fn build_asset(self, name: &str, assets: &mut Assets<BindingInstance<T>>) -> Handle<BindingInstance<T>>
121    where
122        BindingInstance<T>: AssetSource,
123    {
124        self.validate();
125        assets.insert(name, self)
126    }
127}
128
129/// Looks up a target's bind group slot index by entry name.
130pub fn binding_index(entries: &[BindingEntry], name: &str) -> Option<u32> {
131    entries.iter().find(|e| e.name == name).map(|e| e.binding)
132}
133
134/// The GPU-resident bind group an uploaded [`BindingInstance`] produces.
135pub struct GPUBindingInstance<T> {
136    pub target: Handle<T>,
137    pub bind_group: BindGroup,
138    buffers: Vec<(&'static str, Buffer)>,
139    dynamic_buffers: Vec<(&'static str, DynamicBuffer)>,
140    _marker: PhantomData<fn() -> T>,
141}
142
143impl<T> GPUBindingInstance<T> {
144    /// Overwrites a named uniform/storage buffer's contents in place —
145    /// avoids rebuilding the whole bind group for a per-frame update.
146    pub fn update(&self, name: &str, data: &[u8]) {
147        match self.buffer(name) {
148            Some(buf) => buf.write(data),
149            None => tracing::warn!(
150                "GPUBindingInstance::update: no bound buffer named '{name}' — check for a typo \
151                 against the entries in this instance's BindingInstance"
152            ),
153        }
154    }
155
156    pub fn buffer(&self, name: &str) -> Option<&Buffer> {
157        self.buffers.iter().find(|(n, _)| *n == name).map(|(_, buf)| buf)
158    }
159
160    /// Same as `.buffer`, for a binding made via `.with_dynamic_buffer` —
161    /// use `DynamicBuffer::write_element` on the result to update one
162    /// element in place.
163    pub fn dynamic_buffer(&self, name: &str) -> Option<&DynamicBuffer> {
164        self.dynamic_buffers.iter().find(|(n, _)| *n == name).map(|(_, buf)| buf)
165    }
166}
167
168impl<T> AssetSource for BindingInstance<T>
169where
170    T: Asset<Backend>,
171    T::Processed: BindGroupTarget,
172{
173    type Processed = GPUBindingInstance<T>;
174}
175
176impl<T> Asset<Backend> for BindingInstance<T>
177where
178    T: Asset<Backend>,
179    T::Processed: BindGroupTarget,
180{
181    type Deps<'a> = (
182        Read<'a, Assets<T>>,
183        Read<'a, Assets<Texture>>,
184        Read<'a, Assets<TextureArray>>,
185        Read<'a, Assets<Cubemap>>,
186        Read<'a, GlobalSamplers>,
187    );
188
189    fn upload<'a>(&self, backend: &Backend, deps: &Self::Deps<'a>) -> Option<GPUBindingInstance<T>> {
190        let (targets, textures, texture_arrays, cubemaps, samplers) = deps;
191        let target = targets.get(self.target)?;
192
193        // buffers backing `Uniform`/`Storage` entries are built fresh here;
194        // a `Buffer` entry already exists — just cloned (cheap: it's a
195        // handle to the same GPU buffer) so `GPUBindingInstance` can still
196        // look it up by name later via `.update()`/`.buffer()`.
197        let owned_buffers: Vec<(&'static str, Buffer)> = self
198            .params
199            .iter()
200            .filter_map(|(name, entry)| match entry {
201                BindingInstanceEntry::Uniform(bytes) => Some((
202                    *name,
203                    BufferBuilder::with_data(bytes)
204                        .with_usage(BufferUsages::UNIFORM | BufferUsages::COPY_DST | BufferUsages::COPY_SRC)
205                        .build(backend),
206                )),
207                BindingInstanceEntry::Storage(bytes) => Some((
208                    *name,
209                    BufferBuilder::with_data(bytes)
210                        .with_usage(BufferUsages::STORAGE | BufferUsages::COPY_DST | BufferUsages::COPY_SRC)
211                        .build(backend),
212                )),
213                BindingInstanceEntry::Buffer(buffer) => Some((*name, buffer.clone())),
214                _ => None,
215            })
216            .collect();
217
218        // same idea as `owned_buffers`, for `.with_dynamic_buffer` entries —
219        // kept separate since binding one uses `with_dynamic_buffer_at`,
220        // not `with_buffer_at`.
221        let owned_dynamic_buffers: Vec<(&'static str, DynamicBuffer)> = self
222            .params
223            .iter()
224            .filter_map(|(name, entry)| match entry {
225                BindingInstanceEntry::DynamicBuffer(buffer) => Some((*name, buffer.clone())),
226                _ => None,
227            })
228            .collect();
229
230        let mut builder = BindGroupBuilder::new(target.bind_group_layout());
231        for (name, entry) in &self.params {
232            let binding = binding_index(target.binding_entries(), name)?;
233            builder = match entry {
234                BindingInstanceEntry::Texture(handle) => builder.with_texture_2d_at(binding, textures.get(*handle)?),
235                BindingInstanceEntry::TextureArray(handle) => {
236                    builder.with_texture_array_at(binding, texture_arrays.get(*handle)?)
237                }
238                BindingInstanceEntry::Cubemap(handle) => builder.with_texture_cubemap_at(binding, cubemaps.get(*handle)?),
239                BindingInstanceEntry::Sampler(kind) => builder.with_sampler_at(binding, samplers.get(*kind)),
240                BindingInstanceEntry::Uniform(_) | BindingInstanceEntry::Storage(_) | BindingInstanceEntry::Buffer(_) => {
241                    let buf = &owned_buffers.iter().find(|(n, _)| n == name)?.1;
242                    builder.with_buffer_at(binding, buf)
243                }
244                BindingInstanceEntry::DynamicBuffer(_) => {
245                    let buf = &owned_dynamic_buffers.iter().find(|(n, _)| n == name)?.1;
246                    builder.with_dynamic_buffer_at(binding, buf)
247                }
248            };
249        }
250        let bind_group = builder.build(backend);
251
252        Some(GPUBindingInstance {
253            target: self.target,
254            bind_group,
255            buffers: owned_buffers,
256            dynamic_buffers: owned_dynamic_buffers,
257            _marker: PhantomData,
258        })
259    }
260}
261
262pub type GPUMaterialInstance = GPUBindingInstance<Material>;
263/// A [`Material`]'s bind group — the values a shader actually reads from
264/// (textures, samplers, uniforms) for one draw.
265pub type MaterialInstance = BindingInstance<Material>;
266
267pub type GPUComputeInstance = GPUBindingInstance<Compute>;
268/// A [`Compute`] pipeline's bind group — the buffers/textures it reads and
269/// writes for one dispatch.
270pub type ComputeInstance = BindingInstance<Compute>;