1use std::collections::BTreeMap;
17
18mod buffers;
19pub use buffers::{AnsweredBuffers, BufferExtent, BufferRead};
20
21use iridia::{Program, ResourceId, Stage, SystemValue};
22
23use crate::{
24 Backend, Dim, Element, Executable, ImageLoad, ImageQuery, ImageSample, Resources, Runner, Sample, Sampler,
25};
26
27pub const SAMPLE_STAND_IN: [f32; 4] = [0.5; 4];
29pub const LOAD_STAND_IN: [u32; 4] = [0; 4];
31pub const QUERY_STAND_IN: [u32; 4] = [1; 4];
33
34pub trait BufferReads {
37 fn read(&mut self, view: ResourceId, word: u32, count: u8) -> Result<[u32; 4], String>;
39 fn words(&mut self, view: ResourceId) -> Result<u32, String> {
41 let _ = view;
42 Ok(1 << 20)
43 }
44}
45
46#[derive(Debug)]
48pub struct Zeros;
49
50impl BufferReads for Zeros {
51 fn read(&mut self, _view: ResourceId, _word: u32, _count: u8) -> Result<[u32; 4], String> {
52 Ok([0; 4])
53 }
54}
55
56pub trait Answer<R> {
60 fn sample(&mut self, request: &ImageSample<R>) -> Result<[f32; 4], String>;
62 fn load(&mut self, request: &ImageLoad<R>) -> Result<[u32; 4], String> {
64 let _ = request;
65 Err("a texel load, which the answer does not give".into())
66 }
67 fn query(&mut self, request: &ImageQuery<R>) -> Result<[u32; 4], String> {
69 let _ = request;
70 Err("a size or level-count query, which the answer does not give".into())
71 }
72}
73
74impl<R, F: FnMut(&ImageSample<R>) -> Result<[f32; 4], String>> Answer<R> for F {
76 fn sample(&mut self, request: &ImageSample<R>) -> Result<[f32; 4], String> {
77 self(request)
78 }
79}
80
81#[derive(Clone, Copy, Debug, Default)]
83pub struct StandIns;
84
85impl<R> Answer<R> for StandIns {
86 fn sample(&mut self, _request: &ImageSample<R>) -> Result<[f32; 4], String> {
87 Ok(SAMPLE_STAND_IN)
88 }
89 fn load(&mut self, _request: &ImageLoad<R>) -> Result<[u32; 4], String> {
90 Ok(LOAD_STAND_IN)
91 }
92 fn query(&mut self, _request: &ImageQuery<R>) -> Result<[u32; 4], String> {
93 Ok(QUERY_STAND_IN)
94 }
95}
96
97#[derive(Debug)]
101pub struct AnsweredReads<R, A> {
102 pub answer: A,
104 pub samples: Vec<ImageSample<R>>,
106 pub loads: Vec<ImageLoad<R>>,
108 pub queries: Vec<ImageQuery<R>>,
110}
111
112impl<R, A: Answer<R>> AnsweredReads<R, A> {
113 pub fn new(answer: A) -> Self {
115 AnsweredReads {
116 answer,
117 samples: Vec::new(),
118 loads: Vec::new(),
119 queries: Vec::new(),
120 }
121 }
122
123 pub fn sample(&mut self, request: ImageSample<R>) -> Result<[f32; 4], String> {
125 let value = self.answer.sample(&request)?;
126 self.samples.push(request);
127 Ok(value)
128 }
129
130 pub fn load(&mut self, request: ImageLoad<R>) -> Result<[u32; 4], String> {
132 let value = self.answer.load(&request)?;
133 self.loads.push(request);
134 Ok(value)
135 }
136
137 pub fn query(&mut self, request: ImageQuery<R>) -> Result<[u32; 4], String> {
139 let value = self.answer.query(&request)?;
140 self.queries.push(request);
141 Ok(value)
142 }
143
144 pub fn counts(&self) -> TextureUse {
146 let n = |len: usize| u32::try_from(len).unwrap_or(u32::MAX);
147 TextureUse {
148 samples: n(self.samples.len()),
149 loads: n(self.loads.len()),
150 queries: n(self.queries.len()),
151 }
152 }
153}
154
155impl<A: Answer<u32>> Sampler for AnsweredReads<u32, A> {
156 fn sample(&mut self, request: Sample) -> Result<[f32; 4], String> {
158 let [u, v] = request.uv;
159 AnsweredReads::sample(
160 self,
161 ImageSample {
162 texture: request.texture,
163 sampler: request.sampler,
164 dim: Dim::D2,
165 coord: [u, v, 0.],
166 layer: request.layer,
167 lod: request.lod.into(),
168 offset: [0; 3],
169 gather: None,
170 depth_ref: None,
171 depth: false,
172 },
173 )
174 }
175 fn sample_image(&mut self, request: ImageSample<u32>) -> Result<[f32; 4], String> {
176 AnsweredReads::sample(self, request)
177 }
178 fn load_image(&mut self, request: ImageLoad<u32>) -> Result<[u32; 4], String> {
179 AnsweredReads::load(self, request)
180 }
181 fn query_image(&mut self, request: ImageQuery<u32>) -> Result<[u32; 4], String> {
182 AnsweredReads::query(self, request)
183 }
184}
185
186#[derive(Clone, Copy, Debug, PartialEq)]
188pub struct UvMap {
189 pub scale: [f64; 2],
191 pub offset: [f64; 2],
193}
194
195#[allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)] pub fn affine_uv(points: &[([f32; 2], [f32; 2])]) -> Result<UvMap, String> {
205 if points.len() < 4 {
206 return Err(format!(
207 "{} points: a map is fitted to some and confirmed by a fourth",
208 points.len()
209 ));
210 }
211 let points: Vec<([f64; 2], [f64; 2])> = points
212 .iter()
213 .map(|(i, o)| (i.map(f64::from), o.map(f64::from)))
214 .collect();
215 let on_map = |predicted: f64, terms: f64, sampled: f64| {
216 (predicted - sampled).abs() <= 4. * f64::from(f32::EPSILON) * (terms + sampled.abs())
217 };
218 let Some(&(p0, _)) = points.first() else {
220 return Err("no points".into());
221 };
222 let triangle = (1..points.len()).find_map(|i| {
223 (i + 1..points.len()).find_map(|j| {
224 let (pi, pj) = (points[i].0, points[j].0);
225 let det = (pi[0] - p0[0]) * (pj[1] - p0[1]) - (pi[1] - p0[1]) * (pj[0] - p0[0]);
226 (det != 0.).then_some((i, j, det))
227 })
228 });
229 let Some((i, j, det)) = triangle else {
230 return Err("the inputs are on one line: neither axis is confirmed apart from the other".into());
231 };
232 let mut scale = [0.; 2];
233 let mut offset = [0.; 2];
234 let mut separable = true;
235 for axis in 0..2 {
236 let low = points
237 .iter()
238 .min_by(|a, b| a.0[axis].total_cmp(&b.0[axis]))
239 .ok_or("no points")?;
240 let high = points
241 .iter()
242 .max_by(|a, b| a.0[axis].total_cmp(&b.0[axis]))
243 .ok_or("no points")?;
244 let a = (high.1[axis] - low.1[axis]) / (high.0[axis] - low.0[axis]);
245 let c = low.1[axis] - a * low.0[axis];
246 separable &= points.iter().all(|(input, sampled)| {
247 on_map(
248 a * input[axis] + c,
249 (a * input[axis]).abs() + c.abs(),
250 sampled[axis],
251 )
252 });
253 (scale[axis], offset[axis]) = (a, c);
254 }
255 if separable {
256 return Ok(UvMap { scale, offset });
257 }
258 let (q0, qi, qj) = (points[0], points[i], points[j]);
260 let solve = |axis: usize| {
261 let (du_i, dv_i, ds_i) = (qi.0[0] - q0.0[0], qi.0[1] - q0.0[1], qi.1[axis] - q0.1[axis]);
262 let (du_j, dv_j, ds_j) = (qj.0[0] - q0.0[0], qj.0[1] - q0.0[1], qj.1[axis] - q0.1[axis]);
263 let a = (ds_i * dv_j - dv_i * ds_j) / det;
264 let b = (du_i * ds_j - ds_i * du_j) / det;
265 [a, b, q0.1[axis] - a * q0.0[0] - b * q0.0[1]]
266 };
267 let maps = [solve(0), solve(1)];
268 let affine = points.iter().all(|(input, sampled)| {
269 (0..2).all(|axis| {
270 let [a, b, c] = maps[axis];
271 let terms = (a * input[0]).abs() + (b * input[1]).abs() + c.abs();
272 on_map(a * input[0] + b * input[1] + c, terms, sampled[axis])
273 })
274 });
275 if affine {
276 Err(format!(
277 "an axis takes the other (a rotation or a shear): u from v by {}, v from u by {}",
278 maps[0][1], maps[1][0]
279 ))
280 } else {
281 Err("not affine in the input coordinate (a warp, or a coordinate made from another input or from time)".into())
282 }
283}
284
285#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
287pub struct TextureUse {
288 pub samples: u32,
290 pub loads: u32,
292 pub queries: u32,
295}
296
297impl TextureUse {
298 pub fn none(&self) -> bool {
300 *self == TextureUse::default()
301 }
302
303 pub fn stand_ins(&self) -> Option<String> {
306 if self.none() {
307 return None;
308 }
309 let mut parts = Vec::new();
310 for (n, what, stand_in) in [
311 (self.samples, "samples", format!("{SAMPLE_STAND_IN:?}")),
312 (self.loads, "loads", format!("{LOAD_STAND_IN:?}")),
313 (
314 self.queries,
315 "size and level-count queries",
316 format!("{QUERY_STAND_IN:?}"),
317 ),
318 ] {
319 if n > 0 {
320 parts.push(format!("{n} {what} answered with {stand_in}"));
321 }
322 }
323 Some(format!(
324 "the program reads a texture, answered with stand-ins ({}): a vertex it moves by the texel is not where the \
325 constants alone put it",
326 parts.join(", ")
327 ))
328 }
329
330 pub fn reason(&self, why: String) -> String {
333 match self.stand_ins() {
334 Some(stand_ins) => format!("{why}; {stand_ins}"),
335 None => why,
336 }
337 }
338}
339
340#[derive(Debug)]
343pub struct ProbeRun<B> {
344 pub clip: [f32; 4],
346 pub textures: TextureUse,
348 pub buffers: B,
350}
351
352#[derive(Debug)]
354pub struct VertexProbe {
355 program: Program,
356 executable: Executable,
357 position: usize,
358}
359
360impl VertexProbe {
361 pub fn new(program: Program) -> Result<Self, String> {
363 if program.interface.stage != Stage::Vertex {
364 return Err("the program is not a vertex program".into());
365 }
366 let position = program
367 .interface
368 .outputs
369 .iter()
370 .position(|o| o.system == Some(SystemValue::ClipPosition))
371 .ok_or("the program writes no clip position")?;
372 let executable =
373 Executable::new(&program).map_err(|e| format!("the executor does not take the program: {e}"))?;
374 Ok(VertexProbe {
375 program,
376 executable,
377 position,
378 })
379 }
380
381 pub fn program(&self) -> &Program {
383 &self.program
384 }
385
386 pub fn run<B: BufferReads>(
389 &self,
390 constants: BTreeMap<ResourceId, Vec<[u32; 4]>>,
391 inputs: &[[u32; 4]],
392 buffers: B,
393 ) -> Result<ProbeRun<B>, String> {
394 let mut resources = Resources {
395 constants,
396 ..Resources::default()
397 };
398 for r in &self.program.interface.resources {
399 if let iridia::ResourceKind::ConstantBuffer { layout } = &r.kind {
400 resources
401 .constants
402 .entry(r.id)
403 .or_insert_with(|| vec![[0; 4]; layout.size.div_ceil(16).max(1) as usize]);
404 }
405 }
406 let bound = self
407 .executable
408 .bind_resources(&resources)
409 .map_err(|e| format!("the constants do not bind: {e}"))?;
410 let mut backend = Answers {
411 buffers,
412 textures: AnsweredReads::new(StandIns),
413 };
414 let [out] = Runner::<1>::new(&self.executable, &bound, &mut backend, None)
415 .run([inputs])
416 .map_err(|e| format!("the program does not run: {e}"))?;
417 let clip = out
418 .values
419 .get(self.position)
420 .ok_or("the run gave no clip position")?
421 .map(f32::from_bits);
422 Ok(ProbeRun {
423 clip,
424 textures: backend.textures.counts(),
425 buffers: backend.buffers,
426 })
427 }
428}
429
430struct Answers<B> {
432 buffers: B,
433 textures: AnsweredReads<Element, StandIns>,
434}
435
436impl<B: BufferReads> Backend for Answers<B> {
437 fn sample(&mut self, request: ImageSample<Element>) -> Result<[f32; 4], String> {
438 self.textures.sample(request)
439 }
440 fn load(&mut self, request: ImageLoad<Element>) -> Result<[u32; 4], String> {
441 self.textures.load(request)
442 }
443 fn query(&mut self, request: ImageQuery<Element>) -> Result<[u32; 4], String> {
444 self.textures.query(request)
445 }
446 fn read_buffer(&mut self, view: ResourceId, word: u32, count: u8) -> Result<[u32; 4], String> {
447 self.buffers.read(view, word, count)
448 }
449 fn buffer_words(&mut self, view: ResourceId) -> Result<u32, String> {
450 self.buffers.words(view)
451 }
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457 use crate::Lod;
458
459 fn points(scale: [f32; 2], offset: [f32; 2], inputs: &[[f32; 2]]) -> Vec<([f32; 2], [f32; 2])> {
461 inputs
462 .iter()
463 .map(|&[u, v]| ([u, v], [u * scale[0] + offset[0], v * scale[1] + offset[1]]))
464 .collect()
465 }
466
467 const INPUTS: [[f32; 2]; 4] = [[0.1, 0.2], [0.7, 0.3], [0.4, 0.9], [0.55, 0.45]];
468
469 #[test]
471 fn an_affine_map_is_read_back_from_four_points() {
472 let map = affine_uv(&points([20., 7.5], [-9.5, -3.25], &INPUTS)).unwrap();
473 for (got, want) in map.scale.iter().chain(&map.offset).zip([20., 7.5, -9.5, -3.25]) {
474 assert!((got - want).abs() < 1e-4, "{map:?}");
475 }
476 }
477
478 #[test]
481 fn a_map_that_cannot_be_confirmed_or_is_no_scale_and_offset_is_refused() {
482 let three = points([2., 2.], [0., 0.], &INPUTS[..3]);
483 assert!(affine_uv(&three).unwrap_err().contains("confirmed by a fourth"));
484 let line: Vec<_> = (0..4)
485 .map(|k| {
486 let t = k as f32 * 0.25;
487 ([t, t], [t, t])
488 })
489 .collect();
490 assert!(affine_uv(&line).unwrap_err().contains("one line"));
491 let warp: Vec<_> = INPUTS.iter().map(|&[u, v]| ([u, v], [u * u, v])).collect();
492 assert!(affine_uv(&warp).unwrap_err().contains("not affine"));
493 let turned: Vec<_> = INPUTS.iter().map(|&[u, v]| ([u, v], [v, -u])).collect();
494 assert!(affine_uv(&turned).unwrap_err().contains("rotation or a shear"));
495 }
496
497 #[test]
500 fn answered_reads_answer_and_record_each_read() {
501 let mut reads =
502 AnsweredReads::new(|r: &ImageSample<u32>| Ok([r.coord[0], r.coord[1], r.texture as f32, 1.]));
503 let plain = Sample {
504 texture: 3,
505 sampler: 1,
506 uv: [0.25, 0.75],
507 layer: None,
508 lod: Lod::Level(2.),
509 };
510 assert_eq!(Sampler::sample(&mut reads, plain), Ok([0.25, 0.75, 3., 1.]));
511 let load = ImageLoad {
512 texture: 3,
513 dim: Dim::D2,
514 storage: false,
515 texel: [0; 3],
516 layer: None,
517 sample: None,
518 mip: None,
519 };
520 assert!(reads.load_image(load).unwrap_err().contains("texel load"));
521 let [r] = reads.samples.as_slice() else {
522 panic!("{:?}", reads.samples)
523 };
524 assert_eq!(
525 (r.texture, r.sampler, r.dim, r.coord),
526 (3, 1, Dim::D2, [0.25, 0.75, 0.])
527 );
528 assert_eq!(r.lod, crate::ImageLod::Level(2.));
529 assert_eq!(
530 reads.counts(),
531 TextureUse {
532 samples: 1,
533 loads: 0,
534 queries: 0
535 }
536 );
537 }
538}