Skip to main content

iridia_exec/
evaluate.rs

1//! Evaluate a pixel shader over a UV grid, to bake a procedural material to textures. The caller states everything the
2//! program reads (constant layout and values, a texture per sampled slot, a value per used input) and which output
3//! registers it wants; the evaluator has no game names and no policy, and refuses what it is not given.
4
5use crate::{Absent, Error, PixelShader, Views};
6use iridia::{ConstantType, D3dClass, Interface, Member, ResourceKind};
7use std::collections::BTreeMap;
8
9use primase_foundations::parallel::Threads;
10
11fn error(at: usize, detail: impl Into<String>) -> Error {
12    Error {
13        bytecode: "evaluate",
14        at,
15        detail: detail.into(),
16    }
17}
18
19/// Where a named constant lives, in 32-bit words.
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub struct Field {
22    /// Constant-buffer bind slot (`cb<n>`).
23    pub buffer: u32,
24    /// Word offset in the buffer.
25    pub offset: u32,
26    /// Words the field occupies, padding inside arrays and matrices included.
27    pub words: u32,
28}
29
30/// The constant buffers a program reads and where each named constant lives. Built from the program's interface
31/// ([`Layout::of`]) or by the caller from a game's own records when the bytecode carries none.
32#[derive(Clone, Debug, Default)]
33pub struct Layout {
34    /// Bind slot and size in words of each buffer the program reads.
35    pub buffers: Vec<(u32, u32)>,
36    /// Each named constant's place, by the name D3D reflection gives it.
37    pub fields: BTreeMap<String, Field>,
38}
39impl Layout {
40    /// The program's buffers, and every constant its interface names, by name: a structure's members are flattened to
41    /// their own names, as D3D reflection names them. A name that occurs twice or a constant off a word boundary is
42    /// an error.
43    pub fn of(shader: &dyn PixelShader) -> Result<Self, Error> {
44        let mut layout = Self {
45            buffers: shader
46                .constant_buffers()
47                .iter()
48                .map(|&(slot, rows)| {
49                    rows.checked_mul(4)
50                        .map(|words| (slot, words))
51                        .ok_or_else(|| error(0, "constant buffer size overflow"))
52                })
53                .collect::<Result<_, _>>()?,
54            fields: BTreeMap::new(),
55        };
56        layout.add_named(shader.interface())?;
57        Ok(layout)
58    }
59    fn add_named(&mut self, interface: &Interface) -> Result<(), Error> {
60        for resource in &interface.resources {
61            let ResourceKind::ConstantBuffer { layout } = &resource.kind else {
62                continue;
63            };
64            let slot = resource
65                .provenance
66                .d3d
67                .filter(|b| b.class == D3dClass::ConstantBuffer)
68                .ok_or_else(|| {
69                    error(
70                        0,
71                        format!(
72                            "buffer {} has no constant-buffer register",
73                            resource.provenance.name
74                        ),
75                    )
76                })?
77                .register;
78            for c in &layout.constants {
79                match c.ty.members() {
80                    [] => self.insert(slot, &c.name, c.offset, words(&c.ty)?)?,
81                    members => self.members(slot, c.offset, members)?,
82                }
83            }
84        }
85        Ok(())
86    }
87    fn members(&mut self, slot: u32, offset: u32, members: &[Member]) -> Result<(), Error> {
88        for m in members {
89            let at = offset
90                .checked_add(m.offset)
91                .ok_or_else(|| error(0, "member offset overflow"))?;
92            match m.ty.members() {
93                [] => self.insert(slot, &m.name, at, words(&m.ty)?)?,
94                inner => self.members(slot, at, inner)?,
95            }
96        }
97        Ok(())
98    }
99    fn insert(&mut self, buffer: u32, name: &str, offset: u32, words: u32) -> Result<(), Error> {
100        if !offset.is_multiple_of(4) {
101            return Err(error(
102                offset as usize,
103                format!("field {name} is not word-aligned"),
104            ));
105        }
106        let field = Field {
107            buffer,
108            offset: offset / 4,
109            words,
110        };
111        if self.fields.insert(name.to_owned(), field).is_some() {
112            return Err(error(offset as usize, format!("field name {name} occurs twice")));
113        }
114        Ok(())
115    }
116}
117
118/// Words a constant of this type occupies in its buffer, padding inside arrays and matrices included.
119fn words(ty: &ConstantType) -> Result<u32, Error> {
120    ty.size()
121        .map(|bytes| bytes.div_ceil(4))
122        .ok_or_else(|| error(0, "a constant's layout overflows"))
123}
124
125/// Constant-buffer words of one layout, zero until set.
126#[derive(Clone, Debug)]
127pub struct Constants {
128    fields: BTreeMap<String, Field>,
129    buffers: Vec<(u32, Vec<[u32; 4]>)>,
130    written: Vec<Vec<bool>>,
131}
132impl Constants {
133    /// Constant buffers laid out as `layout` says, every word zero and unset.
134    pub fn new(layout: &Layout) -> Self {
135        Self {
136            fields: layout.fields.clone(),
137            buffers: layout
138                .buffers
139                .iter()
140                .map(|&(slot, words)| (slot, vec![[0; 4]; words.div_ceil(4) as usize]))
141                .collect(),
142            written: layout
143                .buffers
144                .iter()
145                .map(|&(_, words)| vec![false; words.div_ceil(4) as usize * 4])
146                .collect(),
147        }
148    }
149    /// Write a field's leading words. `Ok(false)` when the layout has no such field; more values than the field has
150    /// words is an error. Words past the rows the program reads (or in a buffer it does not read) are not stored:
151    /// the program cannot read them.
152    pub fn set(&mut self, field: &str, values: &[f32]) -> Result<bool, Error> {
153        let Some(&f) = self.fields.get(field) else {
154            return Ok(false);
155        };
156        if values.len() > f.words as usize {
157            return Err(error(
158                0,
159                format!("{} values for {field}, which has {} words", values.len(), f.words),
160            ));
161        }
162        if let Some(b) = self.buffers.iter().position(|(slot, _)| *slot == f.buffer) {
163            let words = self.written.get(b).map_or(0, Vec::len);
164            for (i, v) in values.iter().enumerate() {
165                let word = (f.offset as usize).saturating_add(i);
166                if word < words {
167                    self.store(b, word, v.to_bits());
168                }
169            }
170        }
171        Ok(true)
172    }
173    /// Write raw words at a stated offset of a buffer the program reads (a source that states offsets, integer
174    /// fields, or a caller's own field rules). Words outside the buffer are an error.
175    pub fn set_words(&mut self, buffer: u32, word_offset: u32, words: &[u32]) -> Result<(), Error> {
176        let b = self
177            .buffers
178            .iter()
179            .position(|(slot, _)| *slot == buffer)
180            .ok_or_else(|| error(0, format!("the program reads no cb{buffer}")))?;
181        let end = (word_offset as usize)
182            .checked_add(words.len())
183            .filter(|&end| end <= self.written.get(b).map_or(0, Vec::len))
184            .ok_or_else(|| error(word_offset as usize, format!("words past the end of cb{buffer}")))?;
185        for (word, &bits) in (word_offset as usize..end).zip(words) {
186            self.store(b, word, bits);
187        }
188        Ok(())
189    }
190    fn store(&mut self, b: usize, word: usize, bits: u32) {
191        if let Some(slot) = self
192            .buffers
193            .get_mut(b)
194            .and_then(|(_, rows)| rows.get_mut(word / 4))
195            .and_then(|row| row.get_mut(word % 4))
196        {
197            *slot = bits;
198        }
199        if let Some(written) = self.written.get_mut(b).and_then(|words| words.get_mut(word)) {
200            *written = true;
201        }
202    }
203    /// Every buffer the program reads, by slot, as its rows of four words (zero where nothing was written).
204    pub fn buffers(&self) -> &[(u32, Vec<[u32; 4]>)] {
205        &self.buffers
206    }
207    /// Words never written, padding included: what the caller leaves at zero.
208    pub fn unset_words(&self) -> u64 {
209        self.written.iter().flatten().filter(|w| !**w).count() as u64
210    }
211}
212
213/// One component of an input.
214#[derive(Clone, Copy, Debug, PartialEq)]
215pub enum Lane {
216    /// The 32-bit pattern of the lane.
217    Bits(u32),
218    /// The grid's U (times the period).
219    U,
220    /// The grid's V (times the period).
221    V,
222}
223/// A used input's value. Lane k is the element's k-th component, wherever the signature places the element in its
224/// register (`TEXCOORD1` packed into `.zw` receives lanes 0 and 1 there); lanes past its components are unused.
225#[derive(Clone, Copy, Debug, PartialEq)]
226pub enum Input {
227    /// Four lanes, each its own value or grid coordinate.
228    Lanes([Lane; 4]),
229    /// The same four floats at every pixel.
230    Constant([f32; 4]),
231    /// U, V, 0, 0.
232    GridUv,
233}
234impl Input {
235    fn lanes(self) -> [Lane; 4] {
236        match self {
237            Self::Lanes(lanes) => lanes,
238            Self::Constant(c) => c.map(|v| Lane::Bits(v.to_bits())),
239            Self::GridUv => [Lane::U, Lane::V, Lane::Bits(0), Lane::Bits(0)],
240        }
241    }
242}
243
244/// Pixel (x, y) of a size x size grid is evaluated at U = (x + 0.5) / size * `period[0]`, V likewise with y and
245/// `period[1]`, in f32. The grid is the screen: an input lane taking U steps by `period[0]` / size along x (its d/dx) and
246/// not along y; V likewise along y; other lanes are constant.
247#[derive(Clone, Copy, Debug)]
248pub struct Grid {
249    /// Pixels along each side of the square grid.
250    pub size: u32,
251    /// The span of U and V across the grid: U runs to `period[0]` and V to `period[1]`.
252    pub period: [f32; 2],
253}
254
255/// What [`evaluate`] returns: the requested outputs of every pixel of the grid as floats, and the counts of the run.
256#[derive(Debug)]
257pub struct Evaluation {
258    /// Per requested output: size * size values in row-major order (unrequested components zero), or None when the
259    /// program does not write the requested components.
260    pub targets: Vec<Option<Vec<[f32; 4]>>>,
261    /// Instructions executed, summed over the pixels.
262    pub instructions: u64,
263    /// Samples requested, summed over the pixels.
264    pub samples: u64,
265}
266
267/// How [`evaluate_raw`] runs the program over the grid.
268#[derive(Clone, Copy, Debug, PartialEq, Eq)]
269pub enum Invocation {
270    /// One pixel at a time; a derivative is the grid's step for an input the caller places with [`Lane::U`] or
271    /// [`Lane::V`] (zero for a constant input), and of nothing computed.
272    Scalar,
273    /// 2 x 2 quads, as a GPU runs them ([`PixelShader::execute_quad`]): a derivative is the difference inside the
274    /// quad, of inputs and computed values alike, and an implicit-LOD sample's footprint the quad's gradients of its
275    /// coordinates. A quad past the grid's last row or column runs its outside lanes as helpers (their coordinates
276    /// continue past the edge), their values not kept.
277    Quads,
278}
279
280/// One row's (or row of quads') kept results: the pixels in the order kept, per pixel its targets' bits and written
281/// lanes, and the counts.
282#[derive(Default)]
283struct Kept {
284    pixels: Vec<usize>,
285    discarded: Vec<bool>,
286    values: Vec<([u32; 4], u8)>,
287    instructions: u64,
288    samples: u64,
289}
290
291/// Every requested output of every pixel, as the program wrote it.
292#[derive(Debug)]
293pub struct Raw {
294    /// Per requested target, size * size pixels in row-major order: the output register's bits and the lanes the
295    /// program wrote of it (within the target's mask).
296    pub pixels: Vec<Vec<([u32; 4], u8)>>,
297    /// Per pixel, in the same order, whether the program discarded it (its outputs are then those written before).
298    pub discarded: Vec<bool>,
299    /// Instructions executed, summed over the pixels.
300    pub instructions: u64,
301    /// Samples requested, summed over the pixels.
302    pub samples: u64,
303}
304
305/// Evaluate `shader` over the grid as `invocation` says, keeping each requested output's bits and written lanes per
306/// pixel ([`Raw`]). `textures` are the views by bind slot (`t<n>`); where they refuse what they do not hold
307/// ([`Absent::Refuse`]), a slot the program samples without one is an error. `inputs` are by (semantic, index),
308/// compared case-insensitively; a used input without one is an error. `targets` are (output register, component
309/// mask).
310pub fn evaluate_raw(
311    shader: &dyn PixelShader,
312    constants: &Constants,
313    textures: &Views,
314    inputs: &BTreeMap<(String, u32), Input>,
315    grid: Grid,
316    targets: &[(u32, u8)],
317    invocation: Invocation,
318) -> Result<Raw, Error> {
319    evaluate_raw_with(
320        Threads::ONE,
321        shader,
322        constants,
323        textures,
324        inputs,
325        grid,
326        targets,
327        invocation,
328    )
329}
330
331/// [`evaluate_raw`] on `threads` threads: the grid's rows (of quads, for [`Invocation::Quads`]) worked one per thread
332/// at a time, each with its own sampler, the pixels and counts gathered in row order: the same [`Raw`] on any count.
333/// The first failing row's error, as the serial run's.
334#[allow(clippy::too_many_arguments)]
335pub fn evaluate_raw_with(
336    threads: Threads,
337    shader: &dyn PixelShader,
338    constants: &Constants,
339    textures: &Views,
340    inputs: &BTreeMap<(String, u32), Input>,
341    grid: Grid,
342    targets: &[(u32, u8)],
343    invocation: Invocation,
344) -> Result<Raw, Error> {
345    if grid.size == 0 || grid.size > 4096 || !grid.period.iter().all(|p| p.is_finite() && *p > 0.) {
346        return Err(error(
347            0,
348            "grid size must be 1..=4096 and its period finite and positive",
349        ));
350    }
351    // a table that refuses what it does not hold refuses a slot the program samples before the run
352    if textures.absent == Absent::Refuse
353        && let Some(slot) = shader.textures().iter().find(|s| !textures.views.contains_key(s))
354    {
355        return Err(error(0, format!("t{slot} is sampled but has no texture")));
356    }
357    if targets.iter().any(|&(_, mask)| mask == 0 || mask > 15) {
358        return Err(error(0, "a target's component mask must be 1..=15"));
359    }
360    // Each used input element's lanes, placed at its register and components.
361    let mut placed: Vec<(usize, usize, Lane)> = Vec::new();
362    let mut registers = vec![[0u32; 4]; shader.input_count()];
363    for element in shader.interface().inputs.iter().filter(|e| e.used) {
364        let (name, index) = (&element.label.name, element.label.index);
365        let key = inputs
366            .iter()
367            .find(|((n, i), _)| n.eq_ignore_ascii_case(name) && *i == index)
368            .map(|(_, input)| *input)
369            .ok_or_else(|| error(0, format!("no value for used input {name}{index}")))?;
370        let d3d = element
371            .provenance
372            .d3d
373            .ok_or_else(|| error(0, format!("input {name}{index} has no D3D register")))?;
374        let register = d3d.register as usize;
375        if register >= registers.len() || d3d.mask == 0 || d3d.mask > 15 {
376            return Err(error(
377                0,
378                format!("input {name}{index} is outside the program's registers"),
379            ));
380        }
381        let components = (0..4).filter(|c| d3d.mask & (1 << c) != 0);
382        for (component, lane) in components.zip(key.lanes()) {
383            match lane {
384                Lane::Bits(bits) => {
385                    if let Some(slot) = registers.get_mut(register).and_then(|r| r.get_mut(component)) {
386                        *slot = bits;
387                    }
388                }
389                _ => placed.push((register, component, lane)),
390            }
391        }
392    }
393    // the inputs' screen derivatives, the grid's steps (lanes not placed are constant)
394    let mut derivatives = vec![[[0u32; 4]; 2]; shader.input_count()];
395    for &(register, component, lane) in &placed {
396        let (axis, step) = if lane == Lane::U {
397            (0, grid.period[0] / grid.size as f32)
398        } else {
399            (1, grid.period[1] / grid.size as f32)
400        };
401        if let Some(slot) = derivatives
402            .get_mut(register)
403            .and_then(|d| d.get_mut(axis))
404            .and_then(|d| d.get_mut(component))
405        {
406            *slot = step.to_bits();
407        }
408    }
409    let refs: Vec<(u32, &[[u32; 4]])> = constants
410        .buffers
411        .iter()
412        .map(|(s, v)| (*s, v.as_slice()))
413        .collect();
414    let size = grid.size as usize;
415    // the registers of pixel (x, y), which may lie past the grid (a quad's helper lanes), written over `r`
416    let place = |x: u32, y: u32, r: &mut [[u32; 4]]| {
417        let u = (x as f32 + 0.5) / grid.size as f32 * grid.period[0];
418        let v = (y as f32 + 0.5) / grid.size as f32 * grid.period[1];
419        r.copy_from_slice(&registers);
420        for &(register, lane, source) in &placed {
421            if let Some(slot) = r.get_mut(register).and_then(|r| r.get_mut(lane)) {
422                *slot = if source == Lane::U { u } else { v }.to_bits();
423            }
424        }
425    };
426    let at = |x: u32, y: u32| -> Vec<[u32; 4]> {
427        let mut r = registers.clone();
428        place(x, y, &mut r);
429        r
430    };
431    let row = |y: u32| -> Result<Kept, Error> {
432        let mut sampler = textures;
433        let mut out = Kept::default();
434        let mut keep = |pixel: usize, result: &crate::Execution| {
435            out.instructions = out.instructions.saturating_add(result.instructions as u64);
436            out.samples = out.samples.saturating_add(result.samples as u64);
437            out.pixels.push(pixel);
438            out.discarded.push(result.discarded);
439            for &(register, mask) in targets {
440                let written = result.output_masks.get(register as usize).copied().unwrap_or(0) & mask;
441                let bits = result.outputs.get(register as usize).copied().unwrap_or([0; 4]);
442                out.values.push((bits, written));
443            }
444        };
445        match invocation {
446            Invocation::Scalar => {
447                // the row's pixels together, each as it evaluates alone, their registers side by side
448                let width = registers.len();
449                let mut row_registers = vec![[0u32; 4]; width.saturating_mul(size)];
450                for (x, r) in (0..grid.size).zip(row_registers.chunks_exact_mut(width.max(1))) {
451                    if let Some(r) = r.get_mut(..width) {
452                        place(x, y, r);
453                    }
454                }
455                let lanes: Vec<&[[u32; 4]]> = (0..size)
456                    .map(|x| {
457                        row_registers
458                            .get(x.saturating_mul(width)..x.saturating_add(1).saturating_mul(width))
459                            .unwrap_or_default()
460                    })
461                    .collect();
462                let first = y.saturating_mul(grid.size) as usize;
463                shader
464                    .execute_lanes(&lanes, &derivatives, &refs, &mut sampler, &mut |x, result| {
465                        keep(first.saturating_add(x), result)
466                    })
467                    .map_err(|(x, mut e)| {
468                        e.detail = format!("pixel {}: {}", first.saturating_add(x), e.detail);
469                        e
470                    })?;
471            }
472            Invocation::Quads => {
473                for x in (0..grid.size).step_by(2) {
474                    let (x1, y1) = (x.saturating_add(1), y.saturating_add(1));
475                    let lanes = [(x, y), (x1, y), (x, y1), (x1, y1)];
476                    let inputs = lanes.map(|(lx, ly)| at(lx, ly));
477                    let results = shader
478                        .execute_quad(
479                            [&inputs[0], &inputs[1], &inputs[2], &inputs[3]],
480                            &refs,
481                            &mut sampler,
482                        )
483                        .map_err(|mut e| {
484                            e.detail = format!("quad at pixel ({x}, {y}): {}", e.detail);
485                            e
486                        })?;
487                    for ((lx, ly), result) in lanes.into_iter().zip(&results) {
488                        if lx < grid.size && ly < grid.size {
489                            keep(ly.saturating_mul(grid.size).saturating_add(lx) as usize, result);
490                        }
491                    }
492                }
493            }
494        }
495        Ok(out)
496    };
497    let rows: Vec<u32> = match invocation {
498        Invocation::Scalar => (0..grid.size).collect(),
499        Invocation::Quads => (0..grid.size).step_by(2).collect(),
500    };
501    let done = primase_foundations::parallel::map(threads, &rows, |_, &y| row(y)).map_err(|p| {
502        error(
503            0,
504            format!("row {}: {}", rows.get(p.item).copied().unwrap_or(0), p.message),
505        )
506    })?;
507    let mut pixels: Vec<Vec<([u32; 4], u8)>> = targets
508        .iter()
509        .map(|_| vec![([0; 4], 0); size.saturating_mul(size)])
510        .collect();
511    let mut discarded = vec![false; size.saturating_mul(size)];
512    let (mut instructions, mut samples) = (0u64, 0u64);
513    for r in done {
514        let kept = r?;
515        instructions = instructions.saturating_add(kept.instructions);
516        samples = samples.saturating_add(kept.samples);
517        for (k, &pixel) in kept.pixels.iter().enumerate() {
518            if let (Some(slot), Some(&was)) = (discarded.get_mut(pixel), kept.discarded.get(k)) {
519                *slot = was;
520            }
521            for (t, target) in pixels.iter_mut().enumerate() {
522                let value = kept.values.get(k.saturating_mul(targets.len()).saturating_add(t));
523                if let (Some(slot), Some(&value)) = (target.get_mut(pixel), value) {
524                    *slot = value;
525                }
526            }
527        }
528    }
529    Ok(Raw {
530        pixels,
531        discarded,
532        instructions,
533        samples,
534    })
535}
536
537/// Evaluate `shader` once per grid pixel ([`Invocation::Scalar`]). `textures` are as [`evaluate_raw`] takes them.
538/// `inputs` are by (semantic, index), compared case-insensitively; a used
539/// input without one is an error. `targets` are (output register, required component mask); a target written at some
540/// pixels only is an error.
541pub fn evaluate(
542    shader: &dyn PixelShader,
543    constants: &Constants,
544    textures: &Views,
545    inputs: &BTreeMap<(String, u32), Input>,
546    grid: Grid,
547    targets: &[(u32, u8)],
548) -> Result<Evaluation, Error> {
549    evaluate_with(Threads::ONE, shader, constants, textures, inputs, grid, targets)
550}
551
552/// [`evaluate`] on `threads` threads ([`evaluate_raw_with`], a row of the grid each at a time): the same evaluation on
553/// any count.
554pub fn evaluate_with(
555    threads: Threads,
556    shader: &dyn PixelShader,
557    constants: &Constants,
558    textures: &Views,
559    inputs: &BTreeMap<(String, u32), Input>,
560    grid: Grid,
561    targets: &[(u32, u8)],
562) -> Result<Evaluation, Error> {
563    let raw = evaluate_raw_with(
564        threads,
565        shader,
566        constants,
567        textures,
568        inputs,
569        grid,
570        targets,
571        Invocation::Scalar,
572    )?;
573    let mut out = Vec::with_capacity(targets.len());
574    for (t, &(register, mask)) in targets.iter().enumerate() {
575        let Some(pixels) = raw.pixels.get(t) else {
576            return Err(error(
577                0,
578                format!("target {t}, where the evaluation has {}", raw.pixels.len()),
579            ));
580        };
581        let writes = pixels.first().is_some_and(|&(_, m)| m == mask);
582        if let Some(pixel) = pixels.iter().position(|&(_, m)| (m == mask) != writes) {
583            return Err(error(
584                pixel,
585                format!("o{register} is written at some pixels only"),
586            ));
587        }
588        out.push(writes.then(|| {
589            pixels
590                .iter()
591                .map(|&(bits, _)| {
592                    std::array::from_fn(|c| {
593                        if mask & (1 << c) != 0 {
594                            f32::from_bits(bits.get(c).copied().unwrap_or(0))
595                        } else {
596                            0.
597                        }
598                    })
599                })
600                .collect()
601        }));
602    }
603    Ok(Evaluation {
604        targets: out,
605        instructions: raw.instructions,
606        samples: raw.samples,
607    })
608}