Skip to main content

primase_core/shader/
binding.rs

1//! `core.shader_binding`: on a material entity, which shader programs draw it and what each thing they read stands
2//! for. It states roles, never frozen values: a bake or a renderer decides what a role is worth (a camera position,
3//! the time, the frame's exposure) and says so.
4
5use serde::Serialize;
6use serde_json::Value;
7
8use crate::shader::{
9    BufferBinding, BufferSource, BufferTable, ConstantBinding, ConstantRole, FrameTexture, InputBinding,
10    ObjectParameter, OutputBinding, OutputRole, VariantKey,
11};
12use iridia_interface::{
13    BufferKind, ConstantBufferBinding, Interface, Label, MatrixLines, RenderState, ResourceId, ResourceKind,
14    SamplerState, ScalarType, Stage, SystemValue, Unshown,
15};
16
17use super::Shader;
18use crate::TextureLayers;
19use crate::components::{Texture, TextureFile};
20use crate::json::JsonError;
21use crate::world::{Component, Entity};
22
23/// What a texture slot samples.
24#[derive(Clone, Debug, PartialEq, Serialize)]
25#[serde(rename_all = "snake_case")]
26pub enum TextureSource {
27    /// A texture entity (`core.texture`, `core.texture_layers` or `core.texture_file`), with the sampler state stored
28    /// with it.
29    Texture { texture: Entity, sampler: SamplerState },
30    /// No texture; the slot reads this value everywhere, and why.
31    Default { value: [f32; 4], why: String },
32    /// An image of the frame being drawn, which the consumer supplies.
33    Frame(FrameTexture),
34    /// Nothing is bound and the source says no default: a consumer must not invent one. The reason retains the
35    /// source's resource identity and the missing evidence, so consumers can explain the refusal.
36    Missing { why: String },
37    /// A view of one of the frame's own targets: only in the binding of a step of the frame's `core.render_graph`
38    /// ([`crate::render_graph`]), whose targets these are. A material's binding never states one. It is read through the
39    /// program's samplers, whose states the stage states ([`StageBinding::samplers`]): a sampler's state belongs to the
40    /// sampler, which one texture may be read through several of.
41    Target { view: crate::render_graph::TargetView },
42}
43/// Where a stage binding stands, which decides what its texture slots may bind.
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub(crate) enum BindingPlace {
46    /// A material's `core.shader_binding`.
47    Material,
48    /// A step of the frame's `core.render_graph`, which may bind the graph's targets.
49    GraphStep,
50}
51/// One texture slot: the texture resource of the shader, the sampler resource it is read through, and what is bound.
52#[derive(Clone, Debug, PartialEq, Serialize)]
53pub struct TextureBinding {
54    pub resource: ResourceId,
55    /// The sampler resource of the shader the program reads this texture through, where it reads it through exactly
56    /// one. `None` where the program only loads it (no sampler), reads it through several (each sampler's state is the
57    /// stage's, [`StageBinding::samplers`]), or the source does not say.
58    pub sampler: Option<ResourceId>,
59    pub source: TextureSource,
60}
61/// The state of one sampler resource of a program, as the source binds it ([`StageBinding::samplers`]).
62#[derive(Clone, Debug, PartialEq, Serialize)]
63pub struct SamplerBinding {
64    /// The sampler resource of the shader.
65    pub resource: ResourceId,
66    /// Its filters, addressing, border colour, comparison and level range, each as the source states it.
67    pub state: SamplerState,
68}
69/// One shader program in a material, with the role of everything it reads and writes.
70#[derive(Clone, Debug, PartialEq, Serialize)]
71pub struct StageBinding {
72    pub stage: Stage,
73    /// The variant of the draw this stage is for, where the source draws one material with different programs of a
74    /// stage depending on the mesh (its vertex layout, whether it is skinned): the key the mesh's own data must equal
75    /// ([`VariantKey`]). A binding holds one stage of each kind, or several of one kind each for a distinct value of one
76    /// key.
77    pub variant: Option<VariantKey>,
78    /// The `core.shader` entity.
79    pub shader: Entity,
80    pub inputs: Vec<InputBinding>,
81    pub constants: Vec<ConstantBinding>,
82    pub textures: Vec<TextureBinding>,
83    /// The state of each sampler resource of the program, where the source binds the program's samplers itself (a step
84    /// of the frame's `core.render_graph`): each a sampler of the shader, once. A sampler with no entry has no state the
85    /// source states. A material's binding states none: its sampler states are its textures'
86    /// ([`TextureSource::Texture`]).
87    pub samplers: Vec<SamplerBinding>,
88    /// The buffers the program indexes at run time (tables of the draw's matrices and instances). A stage that states
89    /// any states every buffer resource of its program (a table, or [`BufferSource::Unknown`] with why); empty for a
90    /// source that states none, whose buffers a consumer takes as [`StageBinding::buffer`] says: unknown, with the
91    /// reason it prints.
92    pub buffers: Vec<BufferBinding>,
93    /// The constant buffers the source binds at other than their declared size ([`ConstantBufferBinding`]); a buffer
94    /// with no entry is bound at its declared size.
95    pub constant_buffers: Vec<ConstantBufferBinding>,
96    pub outputs: Vec<OutputBinding>,
97}
98/// `core.shader_binding`, on a material entity: the shader programs it is drawn with, one per stage, what each reads
99/// and writes, and its render state. Everything a program reads has a role, never a frozen value: textures are the
100/// entities bound, constants are roles with the material's own words as stored, views and frames are named, not
101/// valued. What can't be resolved is `Unknown` with the reason, or `Missing`, so nothing is silently made up.
102#[derive(Clone, Debug, Default, PartialEq, Serialize)]
103pub struct ShaderBinding {
104    /// At most one binding per stage.
105    pub stages: Vec<StageBinding>,
106    pub state: RenderState,
107}
108impl ShaderBinding {
109    /// The binding of this stage.
110    pub fn stage(&self, stage: Stage) -> Option<&StageBinding> {
111        self.stages.iter().find(|s| s.stage == stage)
112    }
113}
114impl StageBinding {
115    /// The `stage` program `shader` with nothing bound yet: no variant, inputs, constants, textures, samplers, buffers,
116    /// constant buffers or outputs. A source fills what it states: `StageBinding { textures, ..StageBinding::new(stage,
117    /// shader) }`.
118    pub fn new(stage: Stage, shader: Entity) -> StageBinding {
119        StageBinding {
120            stage,
121            variant: None,
122            shader,
123            inputs: Vec::new(),
124            constants: Vec::new(),
125            textures: Vec::new(),
126            samplers: Vec::new(),
127            buffers: Vec::new(),
128            constant_buffers: Vec::new(),
129            outputs: Vec::new(),
130        }
131    }
132
133    /// The exact uniform words the source states for this material. Runtime roles and unknown
134    /// fields contribute no words, including padding: a consumer may specialise only this map.
135    pub fn known_words(&self) -> std::collections::BTreeMap<(ResourceId, u32), u32> {
136        let mut out = std::collections::BTreeMap::new();
137        for constant in &self.constants {
138            let words = match &constant.role {
139                ConstantRole::Material(words) | ConstantRole::Default { words, .. } => words,
140                _ => continue,
141            };
142            for (index, word) in words.iter().enumerate() {
143                if let Ok(index) = u32::try_from(index)
144                    && let Some(offset) = index.checked_mul(4)
145                    && offset < constant.size
146                    && let Some(offset) = constant.offset.checked_add(offset)
147                {
148                    out.insert((constant.resource, offset), *word);
149                }
150            }
151        }
152        out
153    }
154
155    /// The state the stage states for sampler `resource` ([`StageBinding::samplers`]).
156    pub fn sampler_state(&self, resource: ResourceId) -> Option<&SamplerState> {
157        self.samplers
158            .iter()
159            .find(|s| s.resource == resource)
160            .map(|s| &s.state)
161    }
162
163    /// The constants whose role is [`ConstantRole::Unknown`], by name (or offset when the source names none) and why.
164    /// A consumer that evaluates the stage uses it to refuse, or to say which values it left out, where zero is not a
165    /// neutral value (a scale, say): it never reads an unknown constant as zero without naming it.
166    pub fn unknown_constants(&self) -> impl Iterator<Item = (String, &str)> {
167        self.constants.iter().filter_map(|c| match &c.role {
168            ConstantRole::Unknown { why } => Some((
169                c.name.clone().unwrap_or_else(|| format!("+{}", c.offset)),
170                why.as_str(),
171            )),
172            _ => None,
173        })
174    }
175
176    /// The lanes the program writes that no output binding states, per output of `interface` (the stage's program's):
177    /// written as its source states it, the D3D signature's lanes less those it never writes (DXBC's `OSGN`, DXIL's
178    /// `OSG1`), in the output's own lanes (bit k its lane k). An output whose source states no written lanes (no D3D
179    /// signature: MojoShader's, SPIR-V's and GLSL's programs) has none here, nor a system value other than a render
180    /// target (a depth, a coverage mask), which states what it is itself.
181    pub fn unbound_written_lanes<'i>(&self, interface: &'i Interface) -> Vec<(&'i Label, u8)> {
182        let mut out = Vec::new();
183        for v in &interface.outputs {
184            if v.system.is_some_and(|s| !matches!(s, SystemValue::Target(_))) {
185                continue;
186            }
187            let Some(d3d) = v.provenance.d3d else {
188                continue;
189            };
190            // the element's own lanes: the register's from its first, as many as it is wide
191            let first = d3d.mask.trailing_zeros();
192            let width = u32::from(v.value.width).min(4);
193            let written = ((d3d.mask & !d3d.read_write_mask) >> first) & ((1u8 << width) - 1);
194            let bound = self
195                .outputs
196                .iter()
197                .filter(|o| o.label == v.label)
198                .fold(0u8, |acc, o| acc | o.lanes);
199            let unbound = written & !bound;
200            if unbound != 0 {
201                out.push((&v.label, unbound));
202            }
203        }
204        out
205    }
206
207    /// The binding agrees with its program: a material's ([`BindingPlace::Material`]) binds no target of a render graph
208    /// and states no sampler of its own.
209    pub(crate) fn verify_in(&self, w: &crate::World, place: BindingPlace) -> Result<(), String> {
210        let shader = w
211            .get::<Shader>(self.shader)
212            .ok_or_else(|| format!("the {:?} stage's shader entity has no core.shader", self.stage))?;
213        let interface: &Interface = &shader.interface;
214        let at = |detail: String| format!("{:?} stage: {detail}", self.stage);
215        if interface.stage != self.stage {
216            return Err(at(format!("its shader is a {:?} program", interface.stage)));
217        }
218        for (n, i) in self.inputs.iter().enumerate() {
219            let Some(varying) = interface.inputs.iter().find(|v| v.label == i.label) else {
220                return Err(at(format!(
221                    "input {}{} is not in the shader",
222                    i.label.name, i.label.index
223                )));
224            };
225            if self.inputs[..n].iter().any(|before| before.label == i.label) {
226                return Err(at(format!(
227                    "input {}{} is bound twice",
228                    i.label.name, i.label.index
229                )));
230            }
231            if let super::InputRole::PreviousOr { fallback, why } = &i.role {
232                if self.stage != Stage::Pixel
233                    || varying.system.is_some()
234                    || varying.value.scalar != ScalarType::Float
235                {
236                    return Err(at("a replacement input must be a float pixel varying".into()));
237                }
238                if why.trim().is_empty() {
239                    return Err(at("a replacement input has no reason".into()));
240                }
241                fallback.check().map_err(at)?;
242            }
243        }
244        for (n, o) in self.outputs.iter().enumerate() {
245            let Some(v) = interface.outputs.iter().find(|v| v.label == o.label) else {
246                return Err(at(format!(
247                    "output {}{} is not in the shader",
248                    o.label.name, o.label.index
249                )));
250            };
251            if o.lanes == 0 || u32::from(o.lanes) >> v.value.width != 0 {
252                return Err(at(format!(
253                    "output {}{} has {} lanes, not the mask {:#x}",
254                    o.label.name, o.label.index, v.value.width, o.lanes
255                )));
256            }
257            if let Some(Err(why)) = o.role.scalar_encoding().map(|e| e.check()) {
258                return Err(at(format!("output {}{}: {why}", o.label.name, o.label.index)));
259            }
260            if let OutputRole::Unknown { why } = &o.role
261                && why.trim().is_empty()
262            {
263                return Err(at(format!(
264                    "output {}{} is Unknown with no why",
265                    o.label.name, o.label.index
266                )));
267            }
268            // one label may carry several roles, each on its own lanes
269            if let Some(before) = self.outputs[..n]
270                .iter()
271                .find(|before| before.label == o.label && before.lanes & o.lanes != 0)
272            {
273                return Err(at(format!(
274                    "output {}{} has two roles on lanes {:#x}",
275                    o.label.name,
276                    o.label.index,
277                    before.lanes & o.lanes
278                )));
279            }
280        }
281        // a pixel program's outputs are the draw's targets: each lane it writes has a role, or is Unknown with why. Any
282        // other stage's outputs are the next stage's inputs, bound there.
283        if self.stage == Stage::Pixel
284            && let Some((label, lanes)) = self.unbound_written_lanes(interface).first()
285        {
286            return Err(at(format!(
287                "output {}{} lanes {lanes:#06b} are written and have no binding",
288                label.name, label.index
289            )));
290        }
291        for c in &self.constants {
292            let Some(ResourceKind::ConstantBuffer { layout }) =
293                interface.resource(c.resource).map(|r| &r.kind)
294            else {
295                return Err(at(format!(
296                    "constant resource {} is not a constant buffer",
297                    c.resource.0
298                )));
299            };
300            if c.size == 0 || c.offset.checked_add(c.size).is_none_or(|end| end > layout.size) {
301                return Err(at(format!(
302                    "constant of {} bytes at {} is outside buffer {} of {} bytes",
303                    c.size, c.offset, c.resource.0, layout.size
304                )));
305            }
306            let named = c.name.as_deref().map_or(String::new(), |n| format!("{n} "));
307            match (c.role.is_matrix(), &c.lines) {
308                (true, None) => {
309                    return Err(at(format!(
310                        "constant {named}at {} is a matrix ({:?}) whose lines are not stated",
311                        c.offset, c.role
312                    )));
313                }
314                (true, Some(MatrixLines::Unshown(Unshown::Unexamined { why }))) if why.trim().is_empty() => {
315                    return Err(at(format!(
316                        "constant {named}at {} is a matrix whose lines are unshown with no reason",
317                        c.offset
318                    )));
319                }
320                (false, Some(lines)) => {
321                    return Err(at(format!(
322                        "constant {named}at {} states lines ({lines:?}) for a role that is no matrix ({:?})",
323                        c.offset, c.role
324                    )));
325                }
326                _ => {}
327            }
328            if let ConstantRole::AlphaReference { value: r, .. } = c.role
329                && (!r.is_finite() || c.size != 4)
330            {
331                return Err(at(format!(
332                    "the alpha reference {r} at {} is not one finite float ({} bytes)",
333                    c.offset, c.size
334                )));
335            }
336            if let ConstantRole::Material(words) = &c.role
337                && words.len() as u64 * 4 > u64::from(c.size)
338            {
339                return Err(at(format!(
340                    "{} material words for a constant of {} bytes at {}",
341                    words.len(),
342                    c.size,
343                    c.offset
344                )));
345            }
346            if let ConstantRole::Default { words, why } = &c.role {
347                if words.len() as u64 * 4 > u64::from(c.size) {
348                    return Err(at(format!(
349                        "{} default words for a constant of {} bytes at {}",
350                        words.len(),
351                        c.size,
352                        c.offset
353                    )));
354                }
355                if why.trim().is_empty() {
356                    return Err(at(format!(
357                        "the default of the constant at {} says no why",
358                        c.offset
359                    )));
360                }
361            }
362        }
363        // the bytes of a constant buffer belong to one constant
364        let mut spans: Vec<(u32, u32, u32)> = self
365            .constants
366            .iter()
367            .map(|c| (c.resource.0, c.offset, c.offset + c.size))
368            .collect();
369        spans.sort_unstable();
370        if let Some(pair) = spans.windows(2).find(|p| p[0].0 == p[1].0 && p[1].1 < p[0].2) {
371            return Err(at(format!(
372                "constants overlap in buffer {}: bytes {}..{} and {}..{}",
373                pair[0].0, pair[0].1, pair[0].2, pair[1].1, pair[1].2
374            )));
375        }
376        for (n, t) in self.textures.iter().enumerate() {
377            if let TextureSource::Missing { why } = &t.source
378                && why.trim().is_empty()
379            {
380                return Err(at(format!(
381                    "texture slot {} is missing without a reason",
382                    t.resource.0
383                )));
384            }
385            let Some(ResourceKind::Texture { dim, arrayed, .. }) =
386                interface.resource(t.resource).map(|r| &r.kind)
387            else {
388                return Err(at(format!("resource {} is not a texture", t.resource.0)));
389            };
390            if self.textures[..n]
391                .iter()
392                .any(|before| before.resource == t.resource)
393            {
394                return Err(at(format!("texture slot {} is bound twice", t.resource.0)));
395            }
396            if place == BindingPlace::Material && matches!(t.source, TextureSource::Target { .. }) {
397                return Err(at(format!(
398                    "texture slot {} binds a render graph target, which only a step of a core.render_graph may",
399                    t.resource.0
400                )));
401            }
402            if let TextureSource::Frame(frame) = &t.source
403                && *dim != frame.dim()
404            {
405                return Err(at(format!(
406                    "texture slot {} is a {dim:?} texture and binds the frame's {frame:?}, a {:?} image",
407                    t.resource.0,
408                    frame.dim()
409                )));
410            }
411            if let Some(s) = t.sampler
412                && !matches!(
413                    interface.resource(s).map(|r| &r.kind),
414                    Some(ResourceKind::Sampler { .. })
415                )
416            {
417                return Err(at(format!("resource {} is not a sampler", s.0)));
418            }
419            if let TextureSource::Texture { texture, .. } = &t.source {
420                if !(w.has::<Texture>(*texture)
421                    || w.has::<TextureLayers>(*texture)
422                    || w.has::<TextureFile>(*texture))
423                {
424                    return Err(at(format!(
425                        "texture slot {} binds an entity that is neither core.texture, core.texture_layers nor core.texture_file",
426                        t.resource.0
427                    )));
428                }
429                // a texture of several images is read as the dimension its shape is
430                if let Some(layers) = w.get::<TextureLayers>(*texture)
431                    && layers.shape.dimension() != (*dim, *arrayed)
432                {
433                    return Err(at(format!(
434                        "texture slot {} is a {dim:?} resource (arrayed {arrayed}) and binds a {:?} texture",
435                        t.resource.0, layers.shape
436                    )));
437                }
438            }
439        }
440        self.verify_samplers(interface, place).map_err(at)?;
441        self.verify_buffers(w, interface, place).map_err(at)?;
442        self.verify_constant_buffers(interface).map_err(at)
443    }
444
445    /// The samplers' states: each a sampler of the shader, once, and a state that agrees with itself
446    /// ([`SamplerState::verify`]); a material's binding states none (its states are its textures').
447    fn verify_samplers(&self, interface: &Interface, place: BindingPlace) -> Result<(), String> {
448        if place == BindingPlace::Material
449            && let Some(first) = self.samplers.first()
450        {
451            return Err(format!(
452                "sampler {} states its own state, which only a step of a core.render_graph may (a material's sampler states are its textures')",
453                first.resource.0
454            ));
455        }
456        for (n, s) in self.samplers.iter().enumerate() {
457            if !matches!(
458                interface.resource(s.resource).map(|r| &r.kind),
459                Some(ResourceKind::Sampler { .. })
460            ) {
461                return Err(format!("resource {} is not a sampler", s.resource.0));
462            }
463            if self.samplers[..n]
464                .iter()
465                .any(|before| before.resource == s.resource)
466            {
467                return Err(format!("sampler {} is stated twice", s.resource.0));
468            }
469            s.state
470                .verify()
471                .map_err(|why| format!("sampler {}: {why}", s.resource.0))?;
472        }
473        Ok(())
474    }
475
476    /// What buffer `resource` of the program holds: its entry, or, where the source states no buffers for the stage,
477    /// unknown with that reason (never zeros without saying so).
478    pub fn buffer(&self, resource: ResourceId) -> BufferSource {
479        self.buffers
480            .iter()
481            .find(|b| b.resource == resource)
482            .map(|b| b.source.clone())
483            .unwrap_or_else(|| BufferSource::Unknown {
484                why: "the source states no buffers for this stage".into(),
485            })
486    }
487
488    /// The constant buffers the source states a binding for: each a constant buffer of the shader, once, bound at a whole
489    /// number of rows no smaller than the program declares.
490    fn verify_constant_buffers(&self, interface: &Interface) -> Result<(), String> {
491        for (n, c) in self.constant_buffers.iter().enumerate() {
492            let Some(ResourceKind::ConstantBuffer { layout }) =
493                interface.resource(c.resource).map(|r| &r.kind)
494            else {
495                return Err(format!("resource {} is not a constant buffer", c.resource.0));
496            };
497            if self.constant_buffers[..n]
498                .iter()
499                .any(|before| before.resource == c.resource)
500            {
501                return Err(format!("constant buffer {} is bound twice", c.resource.0));
502            }
503            if let Some(bytes) = c.bound_size
504                && (bytes % 16 != 0 || bytes < layout.size)
505            {
506                return Err(format!(
507                    "constant buffer {} is bound at {bytes} bytes: not a whole number of 16-byte rows, or smaller than the {} the program declares",
508                    c.resource.0, layout.size
509                ));
510            }
511        }
512        Ok(())
513    }
514
515    /// The bytes the constant buffer `resource` is bound with where the source states a size, else `None` (its
516    /// declared size).
517    pub fn bound_size(&self, resource: ResourceId) -> Option<u32> {
518        self.constant_buffers
519            .iter()
520            .find(|c| c.resource == resource)
521            .and_then(|c| c.bound_size)
522    }
523
524    /// How many matrices the joint palette constant holds as the program reads it: the stage's
525    /// [`ObjectParameter::BonePalette`] constant held as rows (three 16-byte lines a matrix, 48 bytes), at its size or, where
526    /// the source binds its buffer larger than the program declares ([`Self::bound_size`]), the bytes from its offset to the
527    /// bound size, whichever is more. `None` where the stage has no such constant, or holds it as columns or does not show
528    /// which (the lab and the census fill rows only).
529    pub fn bone_palette_matrices(&self) -> Option<u32> {
530        let c = self.constants.iter().find(|c| {
531            matches!(c.role, ConstantRole::Object(ObjectParameter::BonePalette))
532                && matches!(c.lines, Some(MatrixLines::Rows))
533        })?;
534        let bound = self
535            .bound_size(c.resource)
536            .map_or(0, |b| b.saturating_sub(c.offset));
537        Some(c.size.max(bound) / 48)
538    }
539
540    /// The buffer slots: each a buffer of the shader, bound once, a matrix table of 16-byte elements, an instance
541    /// table whose records fit its stride and name a matrix table of this stage, a light table that agrees with
542    /// itself and its buffer's elements, in a world whose lights of its kind have their unit (`core.light_unit`, which
543    /// its colours are in), and light lists that agree with themselves and their buffers, a lookup and an index list
544    /// together, with a light table of each kind the lookup counts; a stage that states any buffer states every buffer
545    /// of its program.
546    fn verify_buffers(
547        &self,
548        w: &crate::World,
549        interface: &Interface,
550        place: BindingPlace,
551    ) -> Result<(), String> {
552        let kind = |id: ResourceId| match interface.resource(id).map(|r| &r.kind) {
553            Some(ResourceKind::Buffer(kind)) => Some(*kind),
554            _ => None,
555        };
556        let lists: Vec<&super::LightListPart> = self
557            .buffers
558            .iter()
559            .filter_map(|b| match &b.source {
560                BufferSource::LightLists(part) => Some(part),
561                _ => None,
562            })
563            .collect();
564        if !lists.is_empty() {
565            let lookups: Vec<_> = lists
566                .iter()
567                .filter_map(|p| match p {
568                    super::LightListPart::Lookup(l) => Some(l),
569                    super::LightListPart::Indices(_) => None,
570                })
571                .collect();
572            let [lookup] = lookups.as_slice() else {
573                return Err(format!(
574                    "light lists with {} lookups: a stage's lists are one lookup and one index list",
575                    lookups.len()
576                ));
577            };
578            if lists.len() != 2 {
579                return Err(format!(
580                    "{} light list buffers: a stage's lists are one lookup and one index list",
581                    lists.len()
582                ));
583            }
584            for (counted, _) in &lookup.counts {
585                if !self
586                    .buffers
587                    .iter()
588                    .any(|b| matches!(&b.source, BufferSource::Lights(t) if t.kind == *counted))
589                {
590                    return Err(format!(
591                        "light lists count {counted:?} lights and the stage binds no {counted:?} light table"
592                    ));
593                }
594            }
595        }
596        for (n, b) in self.buffers.iter().enumerate() {
597            let Some(buffer) = kind(b.resource) else {
598                return Err(format!("resource {} is not a buffer", b.resource.0));
599            };
600            if self.buffers[..n]
601                .iter()
602                .any(|before| before.resource == b.resource)
603            {
604                return Err(format!("buffer slot {} is bound twice", b.resource.0));
605            }
606            match &b.source {
607                BufferSource::Unknown { why } if why.is_empty() => {
608                    return Err(format!(
609                        "buffer slot {} is unknown and says not why",
610                        b.resource.0
611                    ));
612                }
613                BufferSource::Unknown { .. } => {}
614                BufferSource::Graph(_) if place == BindingPlace::Material => {
615                    return Err(format!(
616                        "buffer slot {} binds a render graph buffer, which only a step of a core.render_graph may",
617                        b.resource.0
618                    ));
619                }
620                BufferSource::Graph(_) => {}
621                BufferSource::LightLists(part) => {
622                    let stride = match buffer {
623                        BufferKind::Structured { stride } => Some(stride),
624                        _ => None,
625                    };
626                    part.verify(stride)
627                        .map_err(|why| format!("buffer slot {}: {why}", b.resource.0))?;
628                }
629                BufferSource::Lights(table) => {
630                    let stride = match buffer {
631                        BufferKind::Structured { stride } => Some(stride),
632                        _ => None,
633                    };
634                    table
635                        .verify(stride)
636                        .map_err(|why| format!("buffer slot {}: {why}", b.resource.0))?;
637                    if w.resource::<crate::LightUnit>().is_none()
638                        && w.iter::<crate::components::Light>().any(|(_, l)| table.holds(l))
639                    {
640                        return Err(format!(
641                            "buffer slot {} is a light table in a world whose lights have no core.light_unit: its \
642                             colours have no value",
643                            b.resource.0
644                        ));
645                    }
646                }
647                BufferSource::Table(
648                    BufferTable::ObjectMatrices { rows }
649                    | BufferTable::HeaderedMatrices { rows }
650                    | BufferTable::SkinnedMatrices { rows, .. },
651                ) => {
652                    if !(3..=4).contains(rows) {
653                        return Err(format!(
654                            "buffer slot {} holds matrices of {rows} rows, not 3 or 4",
655                            b.resource.0
656                        ));
657                    }
658                    if let BufferKind::Structured { stride } = buffer
659                        && stride != 16
660                    {
661                        return Err(format!(
662                            "buffer slot {} holds matrix rows of 16 bytes and has elements of {stride}",
663                            b.resource.0
664                        ));
665                    }
666                    if let BufferSource::Table(BufferTable::SkinnedMatrices {
667                        joint_stride, prefix, ..
668                    }) = &b.source
669                    {
670                        if joint_stride < rows {
671                            return Err(format!(
672                                "buffer slot {} holds joints {joint_stride} elements apart with {rows} rows each",
673                                b.resource.0
674                            ));
675                        }
676                        if *prefix == 0 {
677                            return Err(format!(
678                                "buffer slot {} holds skinned matrices with no prefix element to say what the first one holds",
679                                b.resource.0
680                            ));
681                        }
682                    }
683                }
684                BufferSource::Table(BufferTable::Instances {
685                    stride,
686                    matrix_start,
687                    matrices,
688                }) => {
689                    if *stride == 0 || matrix_start >= stride {
690                        return Err(format!(
691                            "buffer slot {} holds instance records of {stride} words and names word {matrix_start}",
692                            b.resource.0
693                        ));
694                    }
695                    if let BufferKind::Structured { stride: bytes } = buffer
696                        && bytes != u32::from(*stride) * 4
697                    {
698                        return Err(format!(
699                            "buffer slot {} holds instance records of {stride} words and has elements of {bytes} bytes",
700                            b.resource.0
701                        ));
702                    }
703                    let is_matrices = |other: &BufferBinding| {
704                        other.resource == *matrices
705                            && matches!(
706                                other.source,
707                                BufferSource::Table(
708                                    BufferTable::ObjectMatrices { .. }
709                                        | BufferTable::HeaderedMatrices { .. }
710                                        | BufferTable::SkinnedMatrices { .. }
711                                )
712                            )
713                    };
714                    if !self.buffers.iter().any(is_matrices) {
715                        return Err(format!(
716                            "buffer slot {} names slot {} for its matrices, which is no matrix table of this stage",
717                            b.resource.0, matrices.0
718                        ));
719                    }
720                }
721            }
722        }
723        if !self.buffers.is_empty()
724            && let Some(r) = interface.resources.iter().find(|r| {
725                matches!(r.kind, ResourceKind::Buffer(_)) && !self.buffers.iter().any(|b| b.resource == r.id)
726            })
727        {
728            return Err(format!(
729                "buffer slot {} has no entry: a stage that states buffers states every one (a table, or unknown with \
730                 why)",
731                r.id.0
732            ));
733        }
734        Ok(())
735    }
736}
737impl Component for ShaderBinding {
738    const NAME: &'static str = "core.shader_binding";
739    const GEOMETRY: crate::Geometry = crate::Geometry::None;
740    fn to_json(&self) -> Result<Option<Value>, JsonError> {
741        crate::json::to_value(self).map(Some)
742    }
743    fn verify(&self, _e: Entity, w: &crate::World) -> Result<(), String> {
744        for (i, s) in self.stages.iter().enumerate() {
745            if let Some(k) = &s.variant {
746                if k.field.is_empty() {
747                    return Err(format!("the {:?} stage's variant key names no field", s.stage));
748                }
749                if let Some(p) = k.parts.iter().enumerate().find_map(|(n, p)| {
750                    k.parts[..n]
751                        .iter()
752                        .any(|before| before.name == p.name)
753                        .then_some(&p.name)
754                }) {
755                    return Err(format!(
756                        "the variant key {} = {} describes {p:?} twice",
757                        k.field, k.value
758                    ));
759                }
760                // the stages of one variant describe it the same way
761                if let Some(other) = self.stages[..i].iter().find(|o| {
762                    o.variant
763                        .as_ref()
764                        .is_some_and(|b| b.field == k.field && b.value == k.value && b.parts != k.parts)
765                }) {
766                    return Err(format!(
767                        "the {:?} and {:?} stages of {} = {} describe it differently",
768                        other.stage, s.stage, k.field, k.value
769                    ));
770                }
771            }
772            // a stage kind may repeat only under distinct values of one key
773            for other in self.stages[..i].iter().filter(|o| o.stage == s.stage) {
774                let distinct = match (&other.variant, &s.variant) {
775                    (Some(a), Some(b)) => a.field == b.field && a.value != b.value,
776                    _ => false,
777                };
778                if !distinct {
779                    return Err(match &s.variant {
780                        None => format!("the {:?} stage is bound twice", s.stage),
781                        Some(k) => format!(
782                            "the {:?} stage is bound twice for {} = {} (several need distinct values of one key)",
783                            s.stage, k.field, k.value
784                        ),
785                    });
786                }
787            }
788            s.verify_in(w, BindingPlace::Material)?;
789        }
790        self.state.verify()
791    }
792    fn references(&self, out: &mut Vec<Entity>) {
793        for s in &self.stages {
794            out.push(s.shader);
795            for t in &s.textures {
796                if let TextureSource::Texture { texture, .. } = &t.source {
797                    out.push(*texture);
798                }
799            }
800        }
801    }
802    fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
803        crate::world::Linearity::Invariant
804    }
805}
806
807#[cfg(test)]
808mod tests {
809    use super::*;
810    use crate::World;
811    use crate::shader::Attribute;
812    use crate::shader::{InputRole, KeyPart, Occluded, ScalarEncoding, SkinPrefix, ViewParameter};
813    use iridia_interface::AddressMode;
814    use iridia_interface::{
815        BufferLayout, Constant, ConstantType, ResourceProvenance, SampleType, ScalarType, ShaderFormat,
816        TextureDim, Value as Lanes, Varying, VaryingProvenance,
817    };
818    use iridia_interface::{Resource, ResourceKind as Kind};
819    fn varying(name: &str, width: u8) -> Varying {
820        Varying {
821            label: Label {
822                name: name.into(),
823                index: 0,
824            },
825            system: None,
826            value: Lanes {
827                scalar: ScalarType::Float,
828                width,
829            },
830            interp: None,
831            used: true,
832            provenance: VaryingProvenance::default(),
833        }
834    }
835    fn resource(id: u32, kind: Kind) -> Resource {
836        Resource {
837            id: ResourceId(id),
838            kind,
839            count: 1,
840            provenance: ResourceProvenance::default(),
841        }
842    }
843    /// A pixel shader with a constant buffer (0), a texture (1) and a sampler (2).
844    fn shader() -> Shader {
845        Shader {
846            format: ShaderFormat::Dxbc,
847            bytecode: vec![0; 8],
848            interface: Interface {
849                stage: Stage::Pixel,
850                inputs: vec![varying("TEXCOORD", 2)],
851                outputs: vec![varying("SV_Target", 4)],
852                resources: vec![
853                    resource(
854                        0,
855                        Kind::ConstantBuffer {
856                            layout: BufferLayout {
857                                size: 32,
858                                constants: vec![Constant {
859                                    name: "tint".into(),
860                                    offset: 0,
861                                    ty: ConstantType::Vector(Lanes {
862                                        scalar: ScalarType::Float,
863                                        width: 4,
864                                    }),
865                                    default: None,
866                                }],
867                            },
868                        },
869                    ),
870                    resource(
871                        1,
872                        Kind::Texture {
873                            dim: TextureDim::D2,
874                            arrayed: false,
875                            multisampled: false,
876                            sample: SampleType::Float,
877                        },
878                    ),
879                    resource(2, Kind::Sampler { comparison: false }),
880                ],
881                facts: Default::default(),
882            },
883        }
884    }
885    fn binding(shader: Entity, texture: Entity) -> ShaderBinding {
886        ShaderBinding {
887            stages: vec![StageBinding {
888                inputs: vec![InputBinding {
889                    label: Label {
890                        name: "TEXCOORD".into(),
891                        index: 0,
892                    },
893                    role: InputRole::Stream(Attribute::TexCoord(0)),
894                }],
895                constants: vec![
896                    ConstantBinding {
897                        resource: ResourceId(0),
898                        offset: 0,
899                        size: 16,
900                        name: Some("tint".into()),
901                        role: ConstantRole::Material(vec![1.0f32.to_bits(); 4]),
902                        lines: None,
903                    },
904                    ConstantBinding {
905                        resource: ResourceId(0),
906                        offset: 16,
907                        size: 16,
908                        name: None,
909                        role: ConstantRole::Unknown {
910                            why: "cb0[1] is not in the recipe".into(),
911                        },
912                        lines: None,
913                    },
914                ],
915                textures: vec![TextureBinding {
916                    resource: ResourceId(1),
917                    sampler: Some(ResourceId(2)),
918                    source: TextureSource::Texture {
919                        texture,
920                        sampler: SamplerState {
921                            wrap: [Some(AddressMode::Repeat); 3],
922                            ..SamplerState::default()
923                        },
924                    },
925                }],
926                outputs: vec![
927                    OutputBinding {
928                        label: Label {
929                            name: "SV_Target".into(),
930                            index: 0,
931                        },
932                        lanes: 0b0111,
933                        role: OutputRole::Albedo,
934                    },
935                    OutputBinding {
936                        label: Label {
937                            name: "SV_Target".into(),
938                            index: 0,
939                        },
940                        lanes: 0b1000,
941                        role: OutputRole::Occlusion {
942                            of: Occluded::AllLight,
943                            encoding: ScalarEncoding::LINEAR,
944                        },
945                    },
946                ],
947                ..StageBinding::new(Stage::Pixel, shader)
948            }],
949            state: RenderState::default(),
950        }
951    }
952    /// A vertex shader with a constant buffer (0) and structured buffers of 16 (10), 8 (11) and 12 (12) bytes an element.
953    fn buffer_shader() -> Shader {
954        let structured = |id, stride| {
955            resource(
956                id,
957                Kind::Buffer(iridia_interface::BufferKind::Structured { stride }),
958            )
959        };
960        Shader {
961            format: ShaderFormat::Dxbc,
962            bytecode: vec![0; 8],
963            interface: Interface {
964                stage: Stage::Vertex,
965                inputs: Vec::new(),
966                outputs: Vec::new(),
967                resources: vec![
968                    resource(
969                        0,
970                        Kind::ConstantBuffer {
971                            layout: BufferLayout {
972                                size: 16,
973                                constants: Vec::new(),
974                            },
975                        },
976                    ),
977                    structured(10, 16),
978                    structured(11, 8),
979                    structured(12, 12),
980                ],
981                facts: Default::default(),
982            },
983        }
984    }
985    fn buffer_binding(shader: Entity, buffers: Vec<BufferBinding>) -> ShaderBinding {
986        ShaderBinding {
987            stages: vec![StageBinding {
988                buffers,
989                ..StageBinding::new(Stage::Vertex, shader)
990            }],
991            state: RenderState::default(),
992        }
993    }
994    fn matrices(resource: u32, rows: u8) -> BufferBinding {
995        BufferBinding {
996            resource: ResourceId(resource),
997            source: BufferSource::Table(BufferTable::ObjectMatrices { rows }),
998        }
999    }
1000    fn headered(resource: u32, rows: u8) -> BufferBinding {
1001        BufferBinding {
1002            resource: ResourceId(resource),
1003            source: BufferSource::Table(BufferTable::HeaderedMatrices { rows }),
1004        }
1005    }
1006    fn skinned(resource: u32, rows: u8, joint_stride: u8, prefix: u8) -> BufferBinding {
1007        BufferBinding {
1008            resource: ResourceId(resource),
1009            source: BufferSource::Table(BufferTable::SkinnedMatrices {
1010                rows,
1011                joint_stride,
1012                prefix,
1013                first: SkinPrefix::Translation,
1014            }),
1015        }
1016    }
1017    fn instances(resource: u32, stride: u8, matrix_start: u8, table: u32) -> BufferBinding {
1018        BufferBinding {
1019            resource: ResourceId(resource),
1020            source: BufferSource::Table(BufferTable::Instances {
1021                stride,
1022                matrix_start,
1023                matrices: ResourceId(table),
1024            }),
1025        }
1026    }
1027    /// A render graph's buffer is a buffer only a step of the graph binds: a material's binding that states one is refused,
1028    /// and the same stage verifies in a graph step's place.
1029    #[test]
1030    fn a_material_binds_no_render_graph_buffer() {
1031        let mut w = World::new();
1032        let (s, m) = (w.spawn(), w.spawn());
1033        w.insert(s, buffer_shader());
1034        let graph = |resource: u32| BufferBinding {
1035            resource: ResourceId(resource),
1036            source: BufferSource::Graph(0),
1037        };
1038        let b = buffer_binding(s, vec![graph(10), graph(11), graph(12)]);
1039        w.insert(m, b.clone());
1040        let err = w.get::<ShaderBinding>(m).unwrap().verify(m, &w).unwrap_err();
1041        assert!(
1042            err.contains(
1043                "buffer slot 10 binds a render graph buffer, which only a step of a core.render_graph may"
1044            ),
1045            "{err}"
1046        );
1047        assert_eq!(b.stages[0].verify_in(&w, BindingPlace::GraphStep), Ok(()));
1048    }
1049    /// The buffer slots: a matrix table and the instance records that index it verify, each rule broken once is named,
1050    /// and the roles serialize under their own names.
1051    #[test]
1052    fn buffer_slots_name_the_tables_the_program_indexes() {
1053        let mut w = World::new();
1054        let (s, m) = (w.spawn(), w.spawn());
1055        w.insert(s, buffer_shader());
1056        // every buffer named: 12 is unknown with a reason
1057        let rest = || BufferBinding {
1058            resource: ResourceId(12),
1059            source: BufferSource::Unknown {
1060                why: "not read here".into(),
1061            },
1062        };
1063        let good = vec![matrices(10, 3), instances(11, 2, 0, 10), rest()];
1064        w.insert(m, buffer_binding(s, good.clone()));
1065        assert_eq!(w.get::<ShaderBinding>(m).unwrap().verify(m, &w), Ok(()));
1066        let json = w.get::<ShaderBinding>(m).unwrap().to_json().unwrap().unwrap();
1067        assert_eq!(
1068            json["stages"][0]["buffers"][0]["source"],
1069            serde_json::json!({"table": {"object_matrices": {"rows": 3}}})
1070        );
1071        assert_eq!(
1072            json["stages"][0]["buffers"][1]["source"],
1073            serde_json::json!({"table": {"instances": {"stride": 2, "matrix_start": 0, "matrices": 10}}})
1074        );
1075        let unknown = |why: &str| BufferBinding {
1076            resource: ResourceId(12),
1077            source: BufferSource::Unknown { why: why.into() },
1078        };
1079        let cases: Vec<(Vec<BufferBinding>, &str)> = vec![
1080            (vec![matrices(0, 3)], "is not a buffer"),
1081            (vec![matrices(99, 3)], "is not a buffer"),
1082            (vec![matrices(10, 3), matrices(10, 4)], "bound twice"),
1083            (vec![matrices(10, 5)], "not 3 or 4"),
1084            (vec![matrices(11, 3)], "has elements of 8"),
1085            (vec![matrices(10, 3), instances(11, 2, 2, 10)], "names word 2"),
1086            (vec![matrices(10, 3), instances(11, 0, 0, 10)], "names word 0"),
1087            (
1088                vec![matrices(10, 3), instances(11, 3, 0, 10)],
1089                "has elements of 8 bytes",
1090            ),
1091            (
1092                vec![matrices(10, 3), instances(11, 2, 0, 12)],
1093                "no matrix table of this stage",
1094            ),
1095            (vec![instances(11, 2, 0, 10)], "no matrix table of this stage"),
1096            (
1097                vec![matrices(10, 3), instances(11, 2, 0, 11)],
1098                "no matrix table of this stage",
1099            ),
1100            (vec![unknown("")], "says not why"),
1101        ];
1102        for (buffers, message) in cases {
1103            w.insert(m, buffer_binding(s, buffers.clone()));
1104            let err = w.get::<ShaderBinding>(m).unwrap().verify(m, &w).unwrap_err();
1105            assert!(err.contains(message), "{buffers:?}: {err}");
1106        }
1107        // an unknown buffer with a reason, beside a table, is a binding
1108        w.insert(
1109            m,
1110            buffer_binding(
1111                s,
1112                vec![
1113                    matrices(10, 4),
1114                    instances(11, 2, 0, 10),
1115                    unknown("the engine fills it"),
1116                ],
1117            ),
1118        );
1119        assert_eq!(w.get::<ShaderBinding>(m).unwrap().verify(m, &w), Ok(()));
1120        // a headered table is a matrix table: records may name it, and it has the same rows and element rules
1121        w.insert(
1122            m,
1123            buffer_binding(s, vec![headered(10, 3), instances(11, 2, 1, 10), rest()]),
1124        );
1125        assert_eq!(w.get::<ShaderBinding>(m).unwrap().verify(m, &w), Ok(()));
1126        let json = w.get::<ShaderBinding>(m).unwrap().to_json().unwrap().unwrap();
1127        assert_eq!(
1128            json["stages"][0]["buffers"][0]["source"],
1129            serde_json::json!({"table": {"headered_matrices": {"rows": 3}}})
1130        );
1131        // a skinned block is a matrix table too, with the joints no closer than their rows and a prefix to describe
1132        w.insert(
1133            m,
1134            buffer_binding(s, vec![skinned(10, 3, 3, 3), instances(11, 2, 1, 10), rest()]),
1135        );
1136        assert_eq!(w.get::<ShaderBinding>(m).unwrap().verify(m, &w), Ok(()));
1137        let json = w.get::<ShaderBinding>(m).unwrap().to_json().unwrap().unwrap();
1138        assert_eq!(
1139            json["stages"][0]["buffers"][0]["source"],
1140            serde_json::json!({"table": {"skinned_matrices": {
1141                "rows": 3, "joint_stride": 3, "prefix": 3, "first": "translation"
1142            }}})
1143        );
1144        for (buffers, message) in [
1145            (
1146                vec![skinned(10, 3, 2, 3), instances(11, 2, 1, 10), rest()],
1147                "elements apart with 3 rows",
1148            ),
1149            (
1150                vec![skinned(10, 3, 3, 0), instances(11, 2, 1, 10), rest()],
1151                "no prefix element to say what the first one holds",
1152            ),
1153            (
1154                vec![skinned(10, 5, 5, 3), instances(11, 2, 1, 10), rest()],
1155                "not 3 or 4",
1156            ),
1157        ] {
1158            w.insert(m, buffer_binding(s, buffers.clone()));
1159            let err = w.get::<ShaderBinding>(m).unwrap().verify(m, &w).unwrap_err();
1160            assert!(err.contains(message), "{buffers:?}: {err}");
1161            // no message holds a run of spaces (a line continuation once ate a newline inside one)
1162            assert!(!err.contains("  "), "{err}");
1163        }
1164        // a stage that states buffers states every one; one that states none leaves each unknown with that reason
1165        w.insert(
1166            m,
1167            buffer_binding(s, vec![matrices(10, 3), instances(11, 2, 0, 10)]),
1168        );
1169        let err = w.get::<ShaderBinding>(m).unwrap().verify(m, &w).unwrap_err();
1170        assert!(err.contains("buffer slot 12 has no entry"), "{err}");
1171        w.insert(m, buffer_binding(s, Vec::new()));
1172        let binding = w.get::<ShaderBinding>(m).unwrap();
1173        assert_eq!(binding.verify(m, &w), Ok(()));
1174        assert_eq!(
1175            binding.stages[0].buffer(ResourceId(10)),
1176            BufferSource::Unknown {
1177                why: "the source states no buffers for this stage".into()
1178            }
1179        );
1180        w.insert(m, buffer_binding(s, good.clone()));
1181        assert_eq!(
1182            w.get::<ShaderBinding>(m).unwrap().stages[0].buffer(ResourceId(10)),
1183            BufferSource::Table(BufferTable::ObjectMatrices { rows: 3 })
1184        );
1185        for (buffers, message) in [
1186            (vec![headered(10, 5)], "not 3 or 4"),
1187            (vec![headered(11, 3)], "has elements of 8"),
1188            (vec![headered(10, 3), matrices(10, 3)], "bound twice"),
1189            (vec![headered(0, 3)], "is not a buffer"),
1190        ] {
1191            w.insert(m, buffer_binding(s, buffers.clone()));
1192            let err = w.get::<ShaderBinding>(m).unwrap().verify(m, &w).unwrap_err();
1193            assert!(err.contains(message), "{buffers:?}: {err}");
1194        }
1195    }
1196    /// The constant buffers a source binds larger than the program declares: a constant buffer of the shader, once, in whole
1197    /// rows and no smaller than the declaration (16 bytes here); `None` is the declared size.
1198    #[test]
1199    fn a_constant_buffer_is_bound_at_a_whole_number_of_rows_no_smaller_than_declared() {
1200        let mut w = World::new();
1201        let (s, m) = (w.spawn(), w.spawn());
1202        w.insert(s, buffer_shader());
1203        let bound = |resource: u32, bound_size| ConstantBufferBinding {
1204            resource: ResourceId(resource),
1205            bound_size,
1206        };
1207        let with = |constant_buffers: Vec<ConstantBufferBinding>| {
1208            let mut b = buffer_binding(s, Vec::new());
1209            b.stages[0].constant_buffers = constant_buffers;
1210            b
1211        };
1212        w.insert(m, with(vec![bound(0, Some(12_288))]));
1213        let binding = w.get::<ShaderBinding>(m).unwrap();
1214        assert_eq!(binding.verify(m, &w), Ok(()));
1215        assert_eq!(binding.stages[0].bound_size(ResourceId(0)), Some(12_288));
1216        let json = binding.to_json().unwrap().unwrap();
1217        assert_eq!(
1218            json["stages"][0]["constant_buffers"][0],
1219            serde_json::json!({"resource": 0, "bound_size": 12288})
1220        );
1221        // the joint palette constant's matrices: its size, or the bound size where the source states one; none when it is
1222        // held as columns
1223        let palette = |lines| ConstantBinding {
1224            resource: ResourceId(0),
1225            offset: 0,
1226            size: 96,
1227            name: None,
1228            role: ConstantRole::Object(ObjectParameter::BonePalette),
1229            lines,
1230        };
1231        let mut stage = binding.stages[0].clone();
1232        stage.constants = vec![palette(Some(MatrixLines::Rows))];
1233        assert_eq!(stage.bone_palette_matrices(), Some(256));
1234        stage.constant_buffers.clear();
1235        assert_eq!(stage.bone_palette_matrices(), Some(2));
1236        stage.constants = vec![palette(Some(MatrixLines::Columns))];
1237        assert_eq!(stage.bone_palette_matrices(), None);
1238        stage.constants.clear();
1239        assert_eq!(stage.bone_palette_matrices(), None);
1240        // the declared size, and a buffer with no entry
1241        w.insert(m, with(vec![bound(0, None)]));
1242        let binding = w.get::<ShaderBinding>(m).unwrap();
1243        assert_eq!(binding.verify(m, &w), Ok(()));
1244        assert_eq!(binding.stages[0].bound_size(ResourceId(0)), None);
1245        assert_eq!(binding.stages[0].bound_size(ResourceId(10)), None);
1246        for (buffers, message) in [
1247            (vec![bound(10, Some(64))], "is not a constant buffer"),
1248            (vec![bound(99, Some(64))], "is not a constant buffer"),
1249            (vec![bound(0, Some(32)), bound(0, None)], "bound twice"),
1250            (vec![bound(0, Some(40))], "not a whole number of 16-byte rows"),
1251            (
1252                vec![bound(0, Some(0))],
1253                "smaller than the 16 the program declares",
1254            ),
1255        ] {
1256            w.insert(m, with(buffers.clone()));
1257            let err = w.get::<ShaderBinding>(m).unwrap().verify(m, &w).unwrap_err();
1258            assert!(err.contains(message), "{buffers:?}: {err}");
1259        }
1260    }
1261    /// A world with a shader, a texture and a material carrying the binding built from them.
1262    fn world() -> (World, Entity, Entity, Entity) {
1263        let mut w = World::new();
1264        let (s, t, m) = (w.spawn(), w.spawn(), w.spawn());
1265        w.insert(s, shader());
1266        w.insert(
1267            t,
1268            TextureFile {
1269                path: "albedo.png".into(),
1270            },
1271        );
1272        w.insert(m, binding(s, t));
1273        (w, s, t, m)
1274    }
1275    #[test]
1276    fn an_unfilled_texture_retains_its_reason_and_cannot_omit_it() {
1277        let (w, _, _, material) = world();
1278        let mut binding = w.get::<ShaderBinding>(material).unwrap().clone();
1279        let reason = "engine texture t3 space 0: the runtime image is not stored in the material";
1280        binding.stages[0].textures[0].source = TextureSource::Missing { why: reason.into() };
1281        assert_eq!(binding.verify(material, &w), Ok(()));
1282        let json = serde_json::to_value(&binding).unwrap();
1283        assert_eq!(
1284            json["stages"][0]["textures"][0]["source"]["missing"]["why"],
1285            reason
1286        );
1287        for why in ["", "  \n"] {
1288            binding.stages[0].textures[0].source = TextureSource::Missing { why: why.into() };
1289            let error = binding.verify(material, &w).unwrap_err();
1290            assert!(
1291                error.contains("texture slot 1 is missing without a reason"),
1292                "{error}"
1293            );
1294        }
1295    }
1296
1297    /// An output's encoding of power or root 0, or of a scale that isn't finite, is refused, naming the output.
1298    #[test]
1299    fn an_encoding_of_no_power_no_root_or_no_finite_scale_is_refused() {
1300        let one = 1f32.to_bits();
1301        for bad in [
1302            ScalarEncoding {
1303                power: 0,
1304                root: 1,
1305                scale_bits: one,
1306            },
1307            ScalarEncoding {
1308                power: 1,
1309                root: 0,
1310                scale_bits: one,
1311            },
1312            ScalarEncoding {
1313                power: 1,
1314                root: 1,
1315                scale_bits: f32::INFINITY.to_bits(),
1316            },
1317        ] {
1318            assert!(bad.check().is_err(), "{bad:?}");
1319            let (mut w, _, _, m) = world();
1320            let mut b = w.get::<ShaderBinding>(m).unwrap().clone();
1321            b.stages[0].outputs[1].role = OutputRole::Occlusion {
1322                of: Occluded::AllLight,
1323                encoding: bad,
1324            };
1325            w.insert(m, b);
1326            let err = w.get::<ShaderBinding>(m).unwrap().verify(m, &w).unwrap_err();
1327            assert!(err.contains("output SV_Target0: an encoding of"), "{err}");
1328        }
1329        assert_eq!(ScalarEncoding::square_of(0.95).check(), Ok(()));
1330        assert_eq!(ScalarEncoding::LINEAR.check(), Ok(()));
1331    }
1332    /// A matrix role's binding states how its lines hold the matrix, and no other role's does: either missing or
1333    /// misplaced is refused, naming the constant.
1334    #[test]
1335    fn a_matrix_role_states_its_lines_and_no_other_role_does() {
1336        let (mut w, _, _, m) = world();
1337        let verify = |w: &mut World, role: ConstantRole, lines: Option<MatrixLines>| {
1338            let mut b = w.get::<ShaderBinding>(m).unwrap().clone();
1339            b.stages[0].constants[0].role = role;
1340            b.stages[0].constants[0].lines = lines;
1341            w.insert(m, b);
1342            w.get::<ShaderBinding>(m).unwrap().verify(m, w)
1343        };
1344        let view_projection = ConstantRole::View(ViewParameter::ViewProjection);
1345        let err = verify(&mut w, view_projection.clone(), None).unwrap_err();
1346        assert!(
1347            err.contains("constant tint at 0 is a matrix") && err.contains("lines are not stated"),
1348            "{err}"
1349        );
1350        let unexamined = |why: &str| MatrixLines::Unshown(Unshown::Unexamined { why: why.into() });
1351        for lines in [
1352            MatrixLines::Rows,
1353            MatrixLines::Columns,
1354            MatrixLines::Unshown(Unshown::NotApplied),
1355            unexamined("no value graph"),
1356        ] {
1357            assert_eq!(verify(&mut w, view_projection.clone(), Some(lines)), Ok(()));
1358        }
1359        let err = verify(&mut w, view_projection.clone(), Some(unexamined(" "))).unwrap_err();
1360        assert!(err.contains("unshown with no reason"), "{err}");
1361        assert_eq!(
1362            verify(
1363                &mut w,
1364                ConstantRole::Object(ObjectParameter::BonePalette),
1365                Some(MatrixLines::Rows)
1366            ),
1367            Ok(())
1368        );
1369        let err = verify(
1370            &mut w,
1371            ConstantRole::Material(vec![0; 4]),
1372            Some(MatrixLines::Rows),
1373        )
1374        .unwrap_err();
1375        assert!(
1376            err.contains("states lines (Rows) for a role that is no matrix"),
1377            "{err}"
1378        );
1379        assert!(!ConstantRole::Object(ObjectParameter::TextureTransformRow(0)).is_matrix());
1380    }
1381    #[test]
1382    fn a_binding_names_what_it_refers_to_and_verifies() {
1383        let (w, s, t, m) = world();
1384        let b = w.get::<ShaderBinding>(m).unwrap();
1385        assert_eq!(b.verify(m, &w), Ok(()));
1386        let mut refs = Vec::new();
1387        b.references(&mut refs);
1388        assert_eq!(refs, [s, t]);
1389        let json = b.to_json().unwrap().unwrap();
1390        assert_eq!(json["stages"][0]["stage"], "pixel");
1391        // the role names what it occludes and how it is stored, the scale as a number
1392        assert_eq!(
1393            json["stages"][0]["outputs"][1]["role"],
1394            serde_json::json!({
1395                "occlusion": {"of": "all_light", "encoding": {"power": 1, "scale": 1.0}}
1396            })
1397        );
1398        assert_eq!(
1399            json["stages"][0]["constants"][1]["role"]["unknown"]["why"],
1400            "cb0[1] is not in the recipe"
1401        );
1402        assert!(b.stage(Stage::Vertex).is_none() && b.stage(Stage::Pixel).is_some());
1403    }
1404    /// Each rule of `verify`, broken once.
1405    #[test]
1406    fn each_broken_reference_is_named() {
1407        let (w, _, _, m) = world();
1408        let good = w.get::<ShaderBinding>(m).unwrap().clone();
1409        type Break = Box<dyn Fn(&mut ShaderBinding)>;
1410        let cases: Vec<(Break, &str)> = vec![
1411            (Box::new(|b| b.stages.push(b.stages[0].clone())), "bound twice"),
1412            (
1413                Box::new(|b| b.stages[0].inputs[0].label.index = 7),
1414                "input TEXCOORD7",
1415            ),
1416            (Box::new(|b| b.stages[0].outputs[0].lanes = 0b10000), "lanes"),
1417            (
1418                Box::new(|b| b.stages[0].constants[0].resource = ResourceId(1)),
1419                "not a constant buffer",
1420            ),
1421            (
1422                Box::new(|b| b.stages[0].constants[1].offset = 24),
1423                "outside buffer",
1424            ),
1425            (
1426                Box::new(|b| b.stages[0].constants[0].role = ConstantRole::Material(vec![0; 5])),
1427                "material words",
1428            ),
1429            (
1430                Box::new(|b| {
1431                    b.stages[0].constants[0].role = ConstantRole::Default {
1432                        words: vec![0; 5],
1433                        why: "a policy".into(),
1434                    }
1435                }),
1436                "default words",
1437            ),
1438            (
1439                Box::new(|b| {
1440                    b.stages[0].constants[0].role = ConstantRole::Default {
1441                        words: vec![0],
1442                        why: " ".into(),
1443                    }
1444                }),
1445                "says no why",
1446            ),
1447            (
1448                Box::new(|b| b.stages[0].textures[0].resource = ResourceId(0)),
1449                "not a texture",
1450            ),
1451            (
1452                Box::new(|b| b.stages[0].textures[0].sampler = Some(ResourceId(1))),
1453                "not a sampler",
1454            ),
1455            (
1456                Box::new(|b| {
1457                    let first = b.stages[0].inputs[0].clone();
1458                    b.stages[0].inputs.push(first);
1459                }),
1460                "input TEXCOORD0 is bound twice",
1461            ),
1462            (
1463                // the fixture's two roles on SV_Target0 are lanes 0b0111 and 0b1000: widening the second overlaps
1464                Box::new(|b| b.stages[0].outputs[1].lanes = 0b1100),
1465                "two roles on lanes 0x4",
1466            ),
1467            (
1468                Box::new(|b| b.stages[0].constants[1].offset = 8),
1469                "constants overlap in buffer 0: bytes 0..16 and 8..24",
1470            ),
1471            (
1472                Box::new(|b| {
1473                    let first = b.stages[0].textures[0].clone();
1474                    b.stages[0].textures.push(first);
1475                }),
1476                "texture slot 1 is bound twice",
1477            ),
1478            (
1479                Box::new(|b| {
1480                    b.stages[0].textures[0].source = TextureSource::Frame(FrameTexture::ReflectionProbe)
1481                }),
1482                "binds the frame's ReflectionProbe, a Cube image",
1483            ),
1484        ];
1485        for (break_it, expect) in cases {
1486            let mut b = good.clone();
1487            break_it(&mut b);
1488            let message = b.verify(m, &w).unwrap_err();
1489            assert!(message.contains(expect), "{expect}: {message}");
1490        }
1491        // an entity that is not a shader, a shader of another stage, a texture slot that binds a non-texture
1492        let mut b = good.clone();
1493        b.stages[0].shader = m;
1494        assert!(b.verify(m, &w).unwrap_err().contains("no core.shader"));
1495        let mut b = good.clone();
1496        b.stages[0].stage = Stage::Vertex;
1497        assert!(b.verify(m, &w).unwrap_err().contains("Pixel program"));
1498        let mut b = good;
1499        if let TextureSource::Texture { texture, .. } = &mut b.stages[0].textures[0].source {
1500            *texture = m;
1501        }
1502        assert!(b.verify(m, &w).unwrap_err().contains("neither core.texture"));
1503    }
1504    /// A pixel program's written lanes each have a binding: lanes its D3D signature writes and no binding states are
1505    /// refused, but not the lanes it never writes, nor a system value other than a target, nor a vertex program's
1506    /// outputs (the next stage's inputs); an Unknown output states why.
1507    #[test]
1508    fn a_pixel_programs_written_lanes_each_have_a_binding() {
1509        let (mut w, s, _, m) = world();
1510        let signature = |w: &mut World, read_write_mask: u8, system: Option<SystemValue>, stage: Stage| {
1511            let mut shader = w.get::<Shader>(s).unwrap().clone();
1512            shader.interface.stage = stage;
1513            let out = &mut shader.interface.outputs[0];
1514            out.system = system;
1515            out.provenance.d3d = Some(iridia_interface::D3dSignature {
1516                register: 0,
1517                mask: 0b1111,
1518                read_write_mask,
1519            });
1520            w.insert(s, shader);
1521        };
1522        let verify = |w: &World, b: &ShaderBinding| b.verify(m, w);
1523        let good = w.get::<ShaderBinding>(m).unwrap().clone();
1524        // the fixture binds lanes 0b0111 and 0b1000 of SV_Target0, all four written
1525        signature(&mut w, 0, None, Stage::Pixel);
1526        assert_eq!(verify(&w, &good), Ok(()));
1527        let mut rgb = good.clone();
1528        rgb.stages[0].outputs.truncate(1);
1529        assert_eq!(
1530            rgb.stages[0].unbound_written_lanes(&w.get::<Shader>(s).unwrap().interface),
1531            [(&good.stages[0].outputs[0].label, 0b1000)]
1532        );
1533        let err = verify(&w, &rgb).unwrap_err();
1534        assert!(
1535            err.contains("output SV_Target0 lanes 0b1000 are written and have no binding"),
1536            "{err}"
1537        );
1538        // the program never writes w
1539        signature(&mut w, 0b1000, None, Stage::Pixel);
1540        assert_eq!(verify(&w, &rgb), Ok(()));
1541        // a target stated as a system value is held the same; a depth is not a target
1542        signature(&mut w, 0, Some(SystemValue::Target(0)), Stage::Pixel);
1543        assert!(verify(&w, &rgb).is_err());
1544        signature(&mut w, 0, Some(SystemValue::Depth), Stage::Pixel);
1545        assert_eq!(verify(&w, &rgb), Ok(()));
1546        // a vertex program's outputs are bound as the pixel program's inputs
1547        signature(&mut w, 0, None, Stage::Vertex);
1548        let mut vertex = rgb.clone();
1549        vertex.stages[0].stage = Stage::Vertex;
1550        assert_eq!(verify(&w, &vertex), Ok(()));
1551        // the w lane Unknown, with why and without
1552        signature(&mut w, 0, None, Stage::Pixel);
1553        let mut unknown = good;
1554        unknown.stages[0].outputs[1].role = OutputRole::unknown("read by nothing examined");
1555        assert_eq!(verify(&w, &unknown), Ok(()));
1556        unknown.stages[0].outputs[1].role = OutputRole::unknown(" ");
1557        let err = verify(&w, &unknown).unwrap_err();
1558        assert!(err.contains("output SV_Target0 is Unknown with no why"), "{err}");
1559    }
1560    /// A frame image on a slot of its own dimension, and a constant colour on any, verify.
1561    #[test]
1562    fn a_frame_image_fits_the_slot_it_is_bound_to() {
1563        let (w, _, _, m) = world();
1564        let mut b = w.get::<ShaderBinding>(m).unwrap().clone();
1565        b.stages[0].textures[0].source = TextureSource::Frame(FrameTexture::SceneColour);
1566        assert_eq!(b.verify(m, &w), Ok(()));
1567        b.stages[0].textures[0].source = TextureSource::Default {
1568            value: [0.5; 4],
1569            why: "a constant".into(),
1570        };
1571        assert_eq!(b.verify(m, &w), Ok(()));
1572        assert_eq!(FrameTexture::LightProbeVolume.dim(), TextureDim::D3);
1573        assert_eq!(FrameTexture::ShadowMap.dim(), TextureDim::D2);
1574    }
1575    /// A render graph's target is a texture only a step of the graph binds: a material's binding that states one is
1576    /// refused, and the same stage verifies in a graph step's place.
1577    #[test]
1578    fn a_material_binds_no_render_graph_target() {
1579        let (w, _, _, m) = world();
1580        let mut b = w.get::<ShaderBinding>(m).unwrap().clone();
1581        b.stages[0].textures[0].source = TextureSource::Target {
1582            view: crate::render_graph::TargetView {
1583                target: 0,
1584                mip: 0,
1585                first_layer: 0,
1586                layers: 0,
1587            },
1588        };
1589        let err = b.verify(m, &w).unwrap_err();
1590        assert!(
1591            err.contains("binds a render graph target, which only a step of a core.render_graph may"),
1592            "{err}"
1593        );
1594        assert_eq!(b.stages[0].verify_in(&w, BindingPlace::GraphStep), Ok(()));
1595    }
1596    /// A sampler's state is stated by a graph step, for a sampler of the shader, once, with a state that agrees with
1597    /// itself; a material's binding states none (its sampler states are its textures'), and a stated state is found by
1598    /// its sampler.
1599    #[test]
1600    fn a_graph_step_states_its_samplers() {
1601        let (w, _, _, m) = world();
1602        let mut b = w.get::<ShaderBinding>(m).unwrap().clone();
1603        let shadow = SamplerState {
1604            wrap: [Some(AddressMode::Border); 3],
1605            compare: Some(iridia_interface::Compare::LessEqual),
1606            border: Some([0.0; 4]),
1607            lod: Some([0.0, 0.0]),
1608            ..SamplerState::default()
1609        };
1610        b.stages[0].samplers = vec![SamplerBinding {
1611            resource: ResourceId(2),
1612            state: shadow,
1613        }];
1614        assert_eq!(b.stages[0].verify_in(&w, BindingPlace::GraphStep), Ok(()));
1615        assert_eq!(b.stages[0].sampler_state(ResourceId(2)), Some(&shadow));
1616        assert_eq!(b.stages[0].sampler_state(ResourceId(1)), None);
1617        let err = b.verify(m, &w).unwrap_err();
1618        assert!(
1619            err.contains("sampler 2 states its own state, which only a step of a core.render_graph may"),
1620            "{err}"
1621        );
1622        let step = |samplers: Vec<SamplerBinding>| {
1623            let mut s = b.stages[0].clone();
1624            s.samplers = samplers;
1625            s.verify_in(&w, BindingPlace::GraphStep)
1626        };
1627        let at = |resource: u32, state: SamplerState| SamplerBinding {
1628            resource: ResourceId(resource),
1629            state,
1630        };
1631        // the texture is no sampler
1632        let err = step(vec![at(1, shadow)]).unwrap_err();
1633        assert!(err.contains("resource 1 is not a sampler"), "{err}");
1634        let err = step(vec![at(2, shadow), at(2, SamplerState::default())]).unwrap_err();
1635        assert!(err.contains("sampler 2 is stated twice"), "{err}");
1636        let reversed = SamplerState {
1637            lod: Some([1.0, 0.0]),
1638            ..shadow
1639        };
1640        let err = step(vec![at(2, reversed)]).unwrap_err();
1641        assert!(err.contains("sampler 2: level range"), "{err}");
1642        // the state belongs to the sampler: it serializes with it
1643        let json = serde_json::to_value(&b.stages[0].samplers[0]).unwrap();
1644        assert_eq!(json["resource"], serde_json::json!(2));
1645        assert_eq!(json["state"]["border"], serde_json::json!([0.0, 0.0, 0.0, 0.0]));
1646        assert_eq!(json["state"]["compare"], serde_json::json!("less_equal"));
1647    }
1648    /// A texture of several images fits a slot read as the dimension its shape is, and no other.
1649    #[test]
1650    fn a_layered_texture_fits_the_slot_of_its_shape() {
1651        use crate::{TexFormat, TextureLayers, TextureShape};
1652        let layers = |shape: TextureShape| TextureLayers {
1653            shape,
1654            width: 1,
1655            height: 1,
1656            format: TexFormat::Rgba8Unorm,
1657            levels: vec![(0..shape.layers_at(0)).map(|_| vec![0; 4]).collect()],
1658            color_space: crate::ColorSpace::Linear,
1659            role: crate::TexRole::Other,
1660            normal: None,
1661        };
1662        let (mut w, s, t, m) = world();
1663        let b = w.get::<ShaderBinding>(m).unwrap().clone();
1664        w.remove::<TextureFile>(t);
1665        // the fixture's slot is a plain 2D texture: no layered shape is that
1666        for shape in [
1667            TextureShape::Array { layers: 2 },
1668            TextureShape::Cube { cubes: 1 },
1669            TextureShape::Volume { depth: 1 },
1670        ] {
1671            w.insert(t, layers(shape));
1672            let why = b.verify(m, &w).unwrap_err();
1673            assert!(
1674                why.contains("is a D2 resource (arrayed false) and binds"),
1675                "{why}"
1676            );
1677        }
1678        // the slot read as an arrayed 2D texture takes an array and nothing else
1679        let Kind::Texture { arrayed, .. } = &mut w.get_mut::<Shader>(s).unwrap().interface.resources[1].kind
1680        else {
1681            panic!("the fixture's second resource is a texture");
1682        };
1683        *arrayed = true;
1684        w.insert(t, layers(TextureShape::Array { layers: 3 }));
1685        assert_eq!(b.verify(m, &w), Ok(()));
1686        w.insert(t, layers(TextureShape::Cube { cubes: 2 }));
1687        assert!(b.verify(m, &w).is_err());
1688    }
1689    /// A variant key names a field, describes each part once, and describes one variant the same way on all its stages.
1690    #[test]
1691    fn a_variant_keys_description_is_consistent() {
1692        let (w, _, _, m) = world();
1693        let part = |name: &str, value: u64| KeyPart {
1694            name: name.into(),
1695            value,
1696        };
1697        let key = |field: &str, value: u32, parts: Vec<KeyPart>| {
1698            Some(VariantKey {
1699                field: field.into(),
1700                value,
1701                parts,
1702            })
1703        };
1704        let good = w.get::<ShaderBinding>(m).unwrap().clone();
1705        let with = |a: Option<VariantKey>| {
1706            let mut b = good.clone();
1707            b.stages[0].variant = a;
1708            b
1709        };
1710        assert_eq!(
1711            with(key("f.variant", 0, vec![part("program", 1), part("outputs", 5)])).verify(m, &w),
1712            Ok(())
1713        );
1714        let message = with(key("", 0, Vec::new())).verify(m, &w).unwrap_err();
1715        assert!(message.contains("names no field"), "{message}");
1716        let message = with(key("f.variant", 0, vec![part("program", 1), part("program", 2)]))
1717            .verify(m, &w)
1718            .unwrap_err();
1719        assert!(message.contains("describes \"program\" twice"), "{message}");
1720        // two stages of one variant (a vertex and a pixel stage share the key) must describe it alike; the pixel
1721        // fixture stands for both, a second stage of another kind is built from it
1722        let mut b = with(key("f.variant", 3, vec![part("program", 1)]));
1723        let mut other = b.stages[0].clone();
1724        other.stage = Stage::Vertex;
1725        other.variant = key("f.variant", 3, vec![part("program", 2)]);
1726        b.stages.push(other);
1727        let message = b.verify(m, &w).unwrap_err();
1728        assert!(message.contains("describe it differently"), "{message}");
1729    }
1730    /// A stage kind repeats only under distinct values of one key: two pixel stages with keys `k = 1` and `k = 2` verify,
1731    /// the same value twice, a key and no key, and two different fields do not.
1732    #[test]
1733    fn a_stage_repeats_only_under_distinct_values_of_one_key() {
1734        let (w, _, _, m) = world();
1735        let good = w.get::<ShaderBinding>(m).unwrap().clone();
1736        let key = |field: &str, value: u32| {
1737            Some(VariantKey {
1738                field: field.to_owned(),
1739                value,
1740                parts: Vec::new(),
1741            })
1742        };
1743        let with = |a: Option<VariantKey>, b: Option<VariantKey>| {
1744            let mut x = good.clone();
1745            x.stages[0].variant = a;
1746            let mut y = x.stages[0].clone();
1747            y.variant = b;
1748            x.stages.push(y);
1749            x
1750        };
1751        assert_eq!(
1752            with(key("g.mesh.kind", 1), key("g.mesh.kind", 2)).verify(m, &w),
1753            Ok(())
1754        );
1755        let same = with(key("g.mesh.kind", 1), key("g.mesh.kind", 1))
1756            .verify(m, &w)
1757            .unwrap_err();
1758        assert!(same.contains("g.mesh.kind = 1"), "{same}");
1759        assert!(with(key("g.mesh.kind", 1), None).verify(m, &w).is_err());
1760        assert!(with(None, key("g.mesh.kind", 1)).verify(m, &w).is_err());
1761        assert!(
1762            with(None, None)
1763                .verify(m, &w)
1764                .unwrap_err()
1765                .contains("bound twice")
1766        );
1767        assert!(
1768            with(key("g.mesh.kind", 1), key("g.mesh.other", 2))
1769                .verify(m, &w)
1770                .is_err()
1771        );
1772        // a third stage repeating the first one's value is refused though it differs from the second's (it is compared
1773        // with every earlier stage of its kind)
1774        let mut three = with(key("g.mesh.kind", 1), key("g.mesh.kind", 2));
1775        let mut z = three.stages[0].clone();
1776        z.variant = key("g.mesh.kind", 1);
1777        three.stages.push(z);
1778        assert!(three.verify(m, &w).is_err());
1779    }
1780}