Skip to main content

unlit_wgpu/
util.rs

1//! Small helpers that have no better home.
2
3use core::hash::{BuildHasher, Hash, Hasher};
4use core::ops::Deref;
5use foldhash::fast::FixedState;
6
7/// The hasher [`Hashed`] computes its words with.
8///
9/// Hashbrown's own hasher, in its fixed-seed state: the crate's hash tables are
10/// hashbrown's and hash with foldhash, so a word stored this way is already in
11/// the shape those tables mix, and independently built hashers agree on the
12/// hash of equal values. [`Hashed`]'s equality reads that word, so a hasher
13/// seeded per instance — `hashbrown::DefaultHashBuilder`, whose default is a
14/// fresh `RandomState`, say — would make equal values compare unequal.
15type FixedHasher = FixedState;
16
17/// A value whose hash is computed once, up front.
18///
19/// Hashing one of these writes the stored word instead of walking the value,
20/// and comparing two of them settles inequality on the word alone. That is
21/// worth doing where a value is large, shared, and hashed once per entity per
22/// frame — see [`VertexLayout`](crate::specialize::VertexLayout).
23///
24/// Equality checks the hash before the value. That is sound because [`Hash`]
25/// requires equal values to hash alike: a hash mismatch therefore means the
26/// values differ, and a hash collision only costs the comparison that would
27/// have run anyway. The hasher is fixed rather than chosen per instance for the
28/// same reason — equal values have to keep hashing alike for the check to hold.
29///
30/// There is no `DerefMut`: editing the value through one would leave the
31/// stored word stale, and equal values with different words would compare
32/// unequal. A mutation goes through [`Hashed::update`] instead, which
33/// recomputes the word after the value changes.
34#[derive(Clone, Debug)]
35pub struct Hashed<V> {
36    /// The value's hash.
37    hash: u64,
38    /// The value the hash was taken from.
39    value: V,
40}
41
42impl<V: Hash> Hashed<V> {
43    /// Compute `value`'s hash once and keep it beside the value.
44    pub fn new(value: V) -> Self {
45        Self {
46            hash: FixedHasher::default().hash_one(&value),
47            value,
48        }
49    }
50
51    /// The hash [`Self::new`] computed.
52    pub fn hash(&self) -> u64 {
53        self.hash
54    }
55
56    /// Rewrite the value in place and recompute the stored word.
57    ///
58    /// The closure receives the value itself, so a caller can apply one of the
59    /// value type's own mutating methods — a `set_`-style rewrite, say — and
60    /// leave the word in step with the value it describes. The recomputation
61    /// costs what [`Self::new`] does, so this is the way to change a value
62    /// that is already built, not a cheaper one.
63    pub fn update(&mut self, rewrite: impl FnOnce(&mut V)) {
64        rewrite(&mut self.value);
65        self.hash = FixedHasher::default().hash_one(&self.value);
66    }
67}
68
69impl<V> Deref for Hashed<V> {
70    type Target = V;
71
72    fn deref(&self) -> &Self::Target {
73        &self.value
74    }
75}
76
77impl<V: PartialEq> PartialEq for Hashed<V> {
78    fn eq(&self, other: &Self) -> bool {
79        self.hash == other.hash && self.value == other.value
80    }
81}
82
83impl<V: Eq> Eq for Hashed<V> {}
84
85impl<V> Hash for Hashed<V> {
86    fn hash<H: Hasher>(&self, state: &mut H) {
87        // Equal values share this word, so writing it satisfies the contract
88        // without touching the value.
89        state.write_u64(self.hash);
90    }
91}
92
93/// Create a shader module from the crate's composed WGSL.
94///
95/// In debug builds the module goes through
96/// [`Device::create_shader_module`](wgpu::Device::create_shader_module),
97/// so wgpu's runtime checks — shader-side bounds checks, loop bounding and
98/// the like — all apply. In release builds it goes through
99/// [`Device::create_shader_module_trusted`](wgpu::Device::create_shader_module_trusted)
100/// with [`ShaderRuntimeChecks::unchecked`](wgpu::ShaderRuntimeChecks::unchecked),
101/// which hands the module to the backend
102/// without injecting any of them. The WGSL this helper accepts is composed at
103/// build time from the crate's own shader package or a caller's entry module,
104/// never arbitrary user code, so the unchecked contract holds: every loop is
105/// bounded, and every binding stays within the region its bind group
106/// describes.
107///
108/// wgpu's own validation always runs either way, and on WebGPU the trusted
109/// path simply calls
110/// [`Device::create_shader_module`](wgpu::Device::create_shader_module) — the browser keeps
111/// checking its modules.
112pub fn shader_module(device: &wgpu::Device, wgsl: &str, label: &str) -> wgpu::ShaderModule {
113    let desc = wgpu::ShaderModuleDescriptor {
114        label: Some(label),
115        source: wgpu::ShaderSource::Wgsl(wgsl.into()),
116    };
117    if cfg!(debug_assertions) {
118        device.create_shader_module(desc)
119    } else {
120        #[expect(
121            unsafe_code,
122            reason = "the composed WGSL is build-time and trusted; see the function docs"
123        )]
124        unsafe {
125            device.create_shader_module_trusted(desc, wgpu::ShaderRuntimeChecks::unchecked())
126        }
127    }
128}
129
130/// Headless device setup shared by the crate's unit tests.
131#[cfg(test)]
132pub(crate) mod test {
133    /// A `(device, queue)` pair on wgpu's noop backend.
134    ///
135    /// The noop backend stubs every GPU operation out — except buffer creation
136    /// and mapping — so it needs no adapter and works everywhere, including
137    /// machines with no GPU at all. That is enough for the unit tests, which
138    /// only exercise validation, resource bookkeeping and command encoding.
139    pub(crate) fn noop_device() -> (wgpu::Device, wgpu::Queue) {
140        wgpu::Device::noop(&wgpu::DeviceDescriptor::default())
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use core::cell::Cell;
147    use std::rc::Rc;
148
149    use super::*;
150
151    #[test]
152    fn equal_values_are_equal_and_hash_alike() {
153        let one = Hashed::new((1u32, "a"));
154        let other = Hashed::new((1u32, "a"));
155
156        assert_eq!(one, other);
157        assert_eq!(
158            one.hash(),
159            other.hash(),
160            "separately built hashers agree on equal values"
161        );
162    }
163
164    #[test]
165    fn different_values_are_not_equal() {
166        assert_ne!(Hashed::new(1u32), Hashed::new(2u32));
167    }
168
169    #[test]
170    fn a_hashed_value_derefs_to_the_value() {
171        let hashed = Hashed::new(vec![1u32, 2, 3]);
172
173        assert_eq!(*hashed, vec![1, 2, 3]);
174        assert_eq!(hashed.len(), 3);
175    }
176
177    /// An update rewrites the value and recomputes the word, so the record
178    /// stays equal to one built fresh from the rewritten value.
179    #[test]
180    fn an_update_rehashes_the_rewritten_value() {
181        let mut hashed = Hashed::new(1u32);
182        hashed.update(|value| *value = 2);
183
184        assert_eq!(hashed, Hashed::new(2u32));
185        assert_eq!(hashed.hash(), FixedHasher::default().hash_one(2u32));
186        assert_eq!(*hashed, 2);
187    }
188
189    /// The stored word is the hash of the value, computed the same way
190    /// [`Hashed::new`] computes it.
191    #[test]
192    fn the_stored_hash_is_the_value_hash() {
193        let hashed = Hashed::new("abc");
194
195        assert_eq!(hashed.hash(), FixedHasher::default().hash_one("abc"));
196    }
197
198    /// A hasher that records the words written into it, and rejects anything
199    /// that would mean the value itself was walked.
200    #[derive(Default)]
201    struct CaptureHasher {
202        written: Vec<u64>,
203    }
204
205    impl Hasher for CaptureHasher {
206        fn finish(&self) -> u64 {
207            0
208        }
209
210        fn write(&mut self, _bytes: &[u8]) {
211            panic!("hashing a Hashed must write its stored word, not the value's bytes");
212        }
213
214        fn write_u64(&mut self, word: u64) {
215            self.written.push(word);
216        }
217    }
218
219    /// Hashing through the trait writes the stored word and stops there, which
220    /// is the whole point of the type.
221    #[test]
222    fn hashing_writes_only_the_stored_word() {
223        let hashed = Hashed::new(vec!["a", "b", "c"]);
224        let mut hasher = CaptureHasher::default();
225        Hash::hash(&hashed, &mut hasher);
226
227        assert_eq!(hasher.written, vec![hashed.hash()]);
228    }
229
230    /// A value that counts how often it is compared, so the hash-first shortcut
231    /// in [`Hashed`]'s equality can be observed.
232    #[derive(Debug)]
233    struct Counted {
234        compares: Rc<Cell<usize>>,
235        value: u32,
236    }
237
238    impl Counted {
239        fn new(compares: &Rc<Cell<usize>>, value: u32) -> Self {
240            Self {
241                compares: Rc::clone(compares),
242                value,
243            }
244        }
245    }
246
247    impl Hash for Counted {
248        fn hash<H: Hasher>(&self, state: &mut H) {
249            self.value.hash(state);
250        }
251    }
252
253    impl PartialEq for Counted {
254        fn eq(&self, other: &Self) -> bool {
255            self.compares.set(self.compares.get() + 1);
256            self.value == other.value
257        }
258    }
259
260    impl Eq for Counted {}
261
262    /// Values that hash differently are settled by the word alone, so they must
263    /// not be compared.
264    #[test]
265    fn unequal_hashes_settle_equality_without_comparing_the_values() {
266        let compares = Rc::new(Cell::new(0));
267        let one = Hashed::new(Counted::new(&compares, 1));
268        let other = Hashed::new(Counted::new(&compares, 2));
269        assert_ne!(one.hash(), other.hash(), "the values hash differently");
270
271        assert_ne!(one, other);
272        assert_eq!(compares.get(), 0, "the values were not compared");
273    }
274
275    /// Equal hashes do reach the value comparison, which is what keeps a hash
276    /// collision from being reported as equality.
277    #[test]
278    fn equal_hashes_still_compare_the_values() {
279        let compares = Rc::new(Cell::new(0));
280        let one = Hashed::new(Counted::new(&compares, 1));
281        let other = Hashed::new(Counted::new(&compares, 1));
282
283        assert_eq!(one, other);
284        assert_eq!(compares.get(), 1, "the values were compared once");
285    }
286}