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}