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},
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}
34
35/// A bind group asset for a [`Material`]/[`Compute`] target — named
36/// textures/samplers/uniforms/storage buffers, matched to the target's
37/// declared entries by name. Usually used via its aliases
38/// [`MaterialInstance`]/[`ComputeInstance`].
39pub struct BindingInstance<T> {
40    target: Handle<T>,
41    params: Vec<(&'static str, BindingInstanceEntry)>,
42    _marker: PhantomData<fn() -> T>,
43}
44
45impl<T> BindingInstance<T>
46where
47    T: Asset<Backend>,
48    T::Processed: BindGroupTarget,
49{
50    pub fn new(target: Handle<T>) -> Self {
51        Self { target, params: Vec::new(), _marker: PhantomData }
52    }
53
54    pub fn with_texture(mut self, name: &'static str, handle: Handle<Texture>) -> Self {
55        self.params.push((name, BindingInstanceEntry::Texture(handle)));
56        self
57    }
58
59    pub fn with_texture_array(mut self, name: &'static str, handle: Handle<TextureArray>) -> Self {
60        self.params.push((name, BindingInstanceEntry::TextureArray(handle)));
61        self
62    }
63
64    pub fn with_cubemap(mut self, name: &'static str, handle: Handle<Cubemap>) -> Self {
65        self.params.push((name, BindingInstanceEntry::Cubemap(handle)));
66        self
67    }
68
69    pub fn with_sampler(mut self, name: &'static str, kind: SamplerKind) -> Self {
70        self.params.push((name, BindingInstanceEntry::Sampler(kind)));
71        self
72    }
73
74    pub fn with_uniform(mut self, name: &'static str, data: Vec<u8>) -> Self {
75        self.params.push((name, BindingInstanceEntry::Uniform(data)));
76        self
77    }
78
79    pub fn with_storage(mut self, name: &'static str, data: Vec<u8>) -> Self {
80        self.params.push((name, BindingInstanceEntry::Storage(data)));
81        self
82    }
83
84    /// Binds an existing [`Buffer`] instead of uploading raw bytes — for a
85    /// buffer you already built yourself (e.g. one a compute pass writes
86    /// to, then another pass reads from). Unlike `.with_uniform`/`.with_storage`,
87    /// no buffer is created here; `buffer` must already carry the usage
88    /// flags this binding needs (`BufferUsages::UNIFORM` or `::STORAGE`,
89    /// matching how the target's own entry for `name` was declared).
90    pub fn with_buffer(mut self, name: &'static str, buffer: Buffer) -> Self {
91        self.params.push((name, BindingInstanceEntry::Buffer(buffer)));
92        self
93    }
94
95    pub fn with_param(mut self, name: &'static str, entry: BindingInstanceEntry) -> Self {
96        self.params.push((name, entry));
97        self
98    }
99
100    fn validate(&self) {
101        if self.params.is_empty() {
102            tracing::warn!(
103                "BindingInstance::new(): no params — this instance won't bind anything \
104                 against its target; did you forget to chain .with_texture(...)/.with_sampler(...)/etc.?"
105            );
106        }
107    }
108
109    pub fn build_asset(self, name: &str, assets: &mut Assets<BindingInstance<T>>) -> Handle<BindingInstance<T>>
110    where
111        BindingInstance<T>: AssetSource,
112    {
113        self.validate();
114        assets.insert(name, self)
115    }
116}
117
118/// Looks up a target's bind group slot index by entry name.
119pub fn binding_index(entries: &[BindingEntry], name: &str) -> Option<u32> {
120    entries.iter().find(|e| e.name == name).map(|e| e.binding)
121}
122
123/// The GPU-resident bind group an uploaded [`BindingInstance`] produces.
124pub struct GPUBindingInstance<T> {
125    pub target: Handle<T>,
126    pub bind_group: BindGroup,
127    buffers: Vec<(&'static str, Buffer)>,
128    _marker: PhantomData<fn() -> T>,
129}
130
131impl<T> GPUBindingInstance<T> {
132    /// Overwrites a named uniform/storage buffer's contents in place —
133    /// avoids rebuilding the whole bind group for a per-frame update.
134    pub fn update(&self, name: &str, data: &[u8]) {
135        match self.buffer(name) {
136            Some(buf) => buf.write(data),
137            None => tracing::warn!(
138                "GPUBindingInstance::update: no bound buffer named '{name}' — check for a typo \
139                 against the entries in this instance's BindingInstance"
140            ),
141        }
142    }
143
144    pub fn buffer(&self, name: &str) -> Option<&Buffer> {
145        self.buffers.iter().find(|(n, _)| *n == name).map(|(_, buf)| buf)
146    }
147}
148
149impl<T> AssetSource for BindingInstance<T>
150where
151    T: Asset<Backend>,
152    T::Processed: BindGroupTarget,
153{
154    type Processed = GPUBindingInstance<T>;
155}
156
157impl<T> Asset<Backend> for BindingInstance<T>
158where
159    T: Asset<Backend>,
160    T::Processed: BindGroupTarget,
161{
162    type Deps<'a> = (
163        Read<'a, Assets<T>>,
164        Read<'a, Assets<Texture>>,
165        Read<'a, Assets<TextureArray>>,
166        Read<'a, Assets<Cubemap>>,
167        Read<'a, GlobalSamplers>,
168    );
169
170    fn upload<'a>(&self, backend: &Backend, deps: &Self::Deps<'a>) -> Option<GPUBindingInstance<T>> {
171        let (targets, textures, texture_arrays, cubemaps, samplers) = deps;
172        let target = targets.get(self.target)?;
173
174        // buffers backing `Uniform`/`Storage` entries are built fresh here;
175        // a `Buffer` entry already exists — just cloned (cheap: it's a
176        // handle to the same GPU buffer) so `GPUBindingInstance` can still
177        // look it up by name later via `.update()`/`.buffer()`.
178        let owned_buffers: Vec<(&'static str, Buffer)> = self
179            .params
180            .iter()
181            .filter_map(|(name, entry)| match entry {
182                BindingInstanceEntry::Uniform(bytes) => Some((
183                    *name,
184                    BufferBuilder::with_data(bytes)
185                        .with_usage(BufferUsages::UNIFORM | BufferUsages::COPY_DST | BufferUsages::COPY_SRC)
186                        .build(backend),
187                )),
188                BindingInstanceEntry::Storage(bytes) => Some((
189                    *name,
190                    BufferBuilder::with_data(bytes)
191                        .with_usage(BufferUsages::STORAGE | BufferUsages::COPY_DST | BufferUsages::COPY_SRC)
192                        .build(backend),
193                )),
194                BindingInstanceEntry::Buffer(buffer) => Some((*name, buffer.clone())),
195                _ => None,
196            })
197            .collect();
198
199        let mut builder = BindGroupBuilder::new(target.bind_group_layout());
200        for (name, entry) in &self.params {
201            let binding = binding_index(target.binding_entries(), name)?;
202            builder = match entry {
203                BindingInstanceEntry::Texture(handle) => builder.with_texture_2d_at(binding, textures.get(*handle)?),
204                BindingInstanceEntry::TextureArray(handle) => {
205                    builder.with_texture_array_at(binding, texture_arrays.get(*handle)?)
206                }
207                BindingInstanceEntry::Cubemap(handle) => builder.with_texture_cubemap_at(binding, cubemaps.get(*handle)?),
208                BindingInstanceEntry::Sampler(kind) => builder.with_sampler_at(binding, samplers.get(*kind)),
209                BindingInstanceEntry::Uniform(_) | BindingInstanceEntry::Storage(_) | BindingInstanceEntry::Buffer(_) => {
210                    let buf = &owned_buffers.iter().find(|(n, _)| n == name)?.1;
211                    builder.with_buffer_at(binding, buf)
212                }
213            };
214        }
215        let bind_group = builder.build(backend);
216
217        Some(GPUBindingInstance { target: self.target, bind_group, buffers: owned_buffers, _marker: PhantomData })
218    }
219}
220
221pub type GPUMaterialInstance = GPUBindingInstance<Material>;
222/// A [`Material`]'s bind group — the values a shader actually reads from
223/// (textures, samplers, uniforms) for one draw.
224pub type MaterialInstance = BindingInstance<Material>;
225
226pub type GPUComputeInstance = GPUBindingInstance<Compute>;
227/// A [`Compute`] pipeline's bind group — the buffers/textures it reads and
228/// writes for one dispatch.
229pub type ComputeInstance = BindingInstance<Compute>;