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