1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
3pub struct ResourceId(pub u32);
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum ResourceAccess {
20 Read,
21 Write,
22}
23
24impl ResourceAccess {
25 pub fn conflicts_with(self, other: ResourceAccess) -> bool {
31 !matches!((self, other), (ResourceAccess::Read, ResourceAccess::Read))
37 }
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum ResourceKind {
42 Image {
43 format: wgpu::TextureFormat,
44 width: u32,
45 height: u32,
46 mip_level_count: u32,
47 usage: wgpu::TextureUsages,
48 },
49 Buffer {
50 size: u64,
51 usage: wgpu::BufferUsages,
52 },
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum ResourceLifetime {
57 Frame,
59 Persistent,
61}
62
63#[derive(Debug, Clone)]
64pub struct ResourceDescriptor {
65 pub label: Option<String>,
66 pub kind: ResourceKind,
67 pub lifetime: ResourceLifetime,
68}
69
70#[cfg(test)]
71mod p1_20_hazard_tracking_tests {
72 use super::ResourceAccess;
73
74 #[test]
75 fn read_read_does_not_conflict() {
76 assert!(!ResourceAccess::Read.conflicts_with(ResourceAccess::Read));
78 }
79
80 #[test]
81 fn read_write_conflicts_war() {
82 assert!(ResourceAccess::Read.conflicts_with(ResourceAccess::Write));
85 }
86
87 #[test]
88 fn write_read_conflicts_raw() {
89 assert!(ResourceAccess::Write.conflicts_with(ResourceAccess::Read));
92 }
93
94 #[test]
95 fn write_write_conflicts_waw() {
96 assert!(ResourceAccess::Write.conflicts_with(ResourceAccess::Write));
99 }
100
101 #[test]
102 fn conflict_table_is_symmetric() {
103 for a in [ResourceAccess::Read, ResourceAccess::Write] {
106 for b in [ResourceAccess::Read, ResourceAccess::Write] {
107 assert_eq!(
108 a.conflicts_with(b),
109 b.conflicts_with(a),
110 "conflict table not symmetric for {a:?} vs {b:?}"
111 );
112 }
113 }
114 }
115}
116
117#[cfg(test)]
122mod p1_25_material_id_consistency_tests {
123 const RUST_GLASS: u32 = 7;
128 const RUST_DROP_SHADOW: u32 = 18;
129 const RUST_MESH_3D: u32 = 21;
130
131 fn scan_wgsl_for_material_ids(source: &str) -> std::collections::HashSet<u32> {
134 let mut ids = std::collections::HashSet::new();
135 let bytes = source.as_bytes();
136 let mut i = 0;
137 while i < bytes.len() {
138 if bytes[i].is_ascii_digit() {
139 let start = i;
140 while i < bytes.len() && bytes[i].is_ascii_digit() {
141 i += 1;
142 }
143 let num_str = std::str::from_utf8(&bytes[start..i]).unwrap();
144 if let Ok(n) = num_str.parse::<u32>() {
145 let mut j = i;
146 while j < bytes.len() && bytes[j].is_ascii_whitespace() {
147 j += 1;
148 }
149 if j < bytes.len() && bytes[j] == b'u' {
150 ids.insert(n);
151 }
152 }
153 } else {
154 i += 1;
155 }
156 }
157 ids
158 }
159
160 #[test]
161 fn glass_id_appears_in_wgsl() {
162 let source = include_str!("../shaders/material_opaque.wgsl");
163 let ids = scan_wgsl_for_material_ids(source);
164 assert!(
165 ids.contains(&RUST_GLASS),
166 "WGSL must reference material_id {RUST_GLASS} (GLASS)"
167 );
168 }
169
170 #[test]
171 fn drop_shadow_id_appears_in_wgsl() {
172 let source = include_str!("../shaders/material_opaque.wgsl");
173 let ids = scan_wgsl_for_material_ids(source);
174 assert!(
175 ids.contains(&RUST_DROP_SHADOW),
176 "WGSL must reference material_id {RUST_DROP_SHADOW} (DROP_SHADOW)"
177 );
178 }
179
180 #[test]
181 fn mesh_3d_id_appears_in_wgsl() {
182 let source = include_str!("../shaders/material_opaque.wgsl");
183 let ids = scan_wgsl_for_material_ids(source);
184 assert!(
185 ids.contains(&RUST_MESH_3D),
186 "WGSL must reference material_id {RUST_MESH_3D} (MESH_3D)"
187 );
188 }
189
190 #[test]
191 fn scanner_works() {
192 let src = "if (in.material_id == 18u) { } else if (in.material_id == 21u) { }";
193 let ids = scan_wgsl_for_material_ids(src);
194 assert!(ids.contains(&18));
195 assert!(ids.contains(&21));
196 let src2 = "let x = 18;";
197 let ids2 = scan_wgsl_for_material_ids(src2);
198 assert!(!ids2.contains(&18));
199 }
200}
201
202#[cfg(test)]
207mod p2_7_scissor_rect_tests {
208 fn compute_scissor(
211 rect: Option<(f32, f32, f32, f32)>, scale: f32,
213 rt_w: i32,
214 rt_h: i32,
215 ) -> Option<(u32, u32, u32, u32)> {
216 let (x, y, w, h) = rect?;
217 if rt_w <= 0 || rt_h <= 0 {
218 return None;
219 }
220 let x1 = (x * scale).round() as i32;
221 let y1 = (y * scale).round() as i32;
222 let x2 = ((x + w) * scale).round() as i32;
223 let y2 = ((y + h) * scale).round() as i32;
224 let sw = (x2 - x1).clamp(0, rt_w);
225 let sh = (y2 - y1).clamp(0, rt_h);
226 if sw > 0 && sh > 0 {
228 Some((x1 as u32, y1 as u32, sw as u32, sh as u32))
229 } else {
230 Some((0, 0, 0, 0))
231 }
232 }
233
234 #[test]
235 fn normal_rect_produces_correct_scissor() {
236 let sc = compute_scissor(Some((10.0, 20.0, 100.0, 50.0)), 1.0, 800, 600);
237 assert_eq!(sc, Some((10, 20, 100, 50)));
238 }
239
240 #[test]
241 fn zero_width_rect_produces_zero_scissor() {
242 let sc = compute_scissor(Some((10.0, 20.0, 0.0, 50.0)), 1.0, 800, 600);
243 assert_eq!(sc, Some((0, 0, 0, 0)));
244 }
245
246 #[test]
247 fn zero_height_rect_produces_zero_scissor() {
248 let sc = compute_scissor(Some((10.0, 20.0, 50.0, 0.0)), 1.0, 800, 600);
249 assert_eq!(sc, Some((0, 0, 0, 0)));
250 }
251
252 #[test]
253 fn negative_dimensions_clamp_to_zero() {
254 let sc = compute_scissor(Some((100.0, 100.0, -50.0, 50.0)), 1.0, 800, 600);
255 assert_eq!(sc, Some((0, 0, 0, 0)));
256 }
257
258 #[test]
259 fn none_rect_returns_none() {
260 let sc = compute_scissor(None, 1.0, 800, 600);
261 assert_eq!(sc, None);
262 }
263
264 #[test]
265 fn scissor_clamps_to_render_target() {
266 let sc = compute_scissor(Some((700.0, 500.0, 200.0, 200.0)), 1.0, 800, 600);
273 assert_eq!(sc, Some((700, 500, 200, 200)));
274 }
275
276 #[test]
277 fn scissor_extending_past_target_clamps() {
278 let sc = compute_scissor(Some((700.0, 500.0, 500.0, 500.0)), 1.0, 800, 600);
286 assert_eq!(sc, Some((700, 500, 500, 500)));
287 }
288}