Skip to main content

primase_core/shader/
roles.rs

1//! What each thing a shader program reads and writes stands for: input, constant, texture, buffer and output roles. It
2//! states roles, never frozen values: whoever evaluates or renders the program (an offline baker, a renderer) decides
3//! what a role is worth (a camera position, the time, the frame's exposure) and says so. The program's own vocabulary
4//! (its interface, resources and render state) is `iridia_interface`'s; what its code shows of a constant or an output
5//! (`MatrixLines`, `AlphaComparison`, `OutputUse`) is read by `iridia`'s analyses, and turned into roles here
6//! ([`state_lines`], [`bind_output_uses`]).
7
8use serde::Serialize;
9
10use iridia_interface::{AlphaComparison, Label, MatrixLines, OutputUse, ResourceId};
11
12use super::encoding::{LaneAffine, NormalEncoding, NormalSpace, ScalarEncoding};
13
14/// A vertex attribute a program's input reads: what the drawn mesh's stream holds.
15#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
16pub enum Attribute {
17    /// The vertex position.
18    Position,
19    /// The vertex normal.
20    Normal,
21    /// Tangent set: xyz tangent, w the bitangent sign. Set 0 is glTF's standard TANGENT.
22    Tangent(u8),
23    /// u right, v down: (0, 0) is the image's top-left corner (glTF).
24    TexCoord(u8),
25    /// Vertex colour set `n`.
26    Color(u8),
27    /// Joint indices set `n` of a skinned mesh.
28    Joints(u8),
29    /// Joint weights set `n` of a skinned mesh.
30    Weights(u8),
31    /// Opaque per-vertex shader data at the game's input index, copied through geometry passes untransformed.
32    /// A consumer that cannot preserve it must refuse ShaderInput by name.
33    ShaderInput(u8),
34    /// A point per vertex in the mesh's own space at the game's input index (a billboard's pivot), moved with the
35    /// positions ([`crate::Semantic::Point`]).
36    Point(u8),
37}
38
39/// What a pixel or vertex stage's input is.
40#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
41#[serde(rename_all = "snake_case")]
42pub enum InputRole {
43    /// A vertex attribute of the drawn mesh.
44    Stream(Attribute),
45    /// An input whose lanes are different vertex attributes the source packed into one register (two texture
46    /// coordinate sets in one four-lane input), or one component of an attribute the source feeds as an input of its own
47    /// (a tangent's sign as a byte): each entry is the attribute, the components it takes and the lanes it fills.
48    Lanes(Vec<LaneStream>),
49    /// The value the previous stage wrote under the same label.
50    Previous,
51    /// The previous stage's value when it executes; otherwise the source-stated replacement input. The reason
52    /// belongs to the binding pass and is reported by a consumer only when it uses the replacement.
53    PreviousOr {
54        fallback: super::InputFallback,
55        why: String,
56    },
57    /// The pipeline supplies it (the varying's system value says which).
58    System,
59    /// Lanes of vertex attributes as [`InputRole::Lanes`] states them, each component stored through `map` (`v * scale +
60    /// bias` of the attribute's own value `v`) and read by the program as a float: a signed unit vector the source stores
61    /// in unsigned normalized bytes is [`LaneAffine::UNORM`], which the program unpacks as `x * 2 - 1`. A lane no entry
62    /// fills is not stated.
63    Mapped {
64        /// The attributes' components and the lanes they fill.
65        lanes: Vec<LaneStream>,
66        /// The map each component is stored through.
67        map: LaneAffine,
68    },
69    /// Values of the drawn instance the source feeds through a vertex input, the same for every vertex of the instance:
70    /// an instance stream the engine fills at draw time, not a stream of the mesh. Each entry is a value and the lanes it
71    /// fills; a lane no entry fills is not stated. A consumer whose vertices already hold the instance's transform (a
72    /// scene in world space) feeds the identity instance.
73    Instance(Vec<InstanceLanes>),
74    /// A corner of the triangle a full-screen step of a render graph draws, which covers the view: the value of the
75    /// corner it holds. The three corners, in vertex order, are at the clip-space positions (-1, 1, 1, 1), (3, 1, 1, 1)
76    /// and (-1, -3, 1, 1), with the texture coordinates (0, 0), (2, 0) and (0, 2): (0, 0) at the view's top-left
77    /// corner, 1 a whole view to the right and down.
78    ScreenTriangle(ScreenTriangleValue),
79    /// Nothing known: the source's own meaning is not resolved, and why.
80    Unknown {
81        /// What was examined and why no role follows.
82        why: String,
83    },
84}
85/// What an input of a full-screen triangle holds of each corner ([`InputRole::ScreenTriangle`]).
86#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
87#[serde(rename_all = "snake_case")]
88pub enum ScreenTriangleValue {
89    /// The corner's clip-space position (four lanes).
90    Position,
91    /// The corner's texture coordinate (two lanes).
92    TextureCoordinate,
93}
94/// The lanes `first..first + count` of an input: `count` components of the vertex attribute `semantic`, from component
95/// `component` (the attribute's own components: x 0, y 1, z 2, w 3), each in the encoding the input's scalar type has, as
96/// the other packed inputs take theirs.
97#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
98pub struct LaneStream {
99    /// The vertex attribute the lanes come from.
100    pub semantic: Attribute,
101    /// The first lane (0 for x).
102    pub first: u8,
103    /// How many lanes, starting at `first`, it fills.
104    pub count: u8,
105    /// The first component of the attribute lane `first` takes (0 for all but the programs that take one component of an
106    /// attribute apart: a source game's cloth programs read the tangent's w, the bitangent sign, as the one
107    /// byte of an input named BINORMAL0, as `(byte - 127) / 127` where the other permutations read TANGENT0.w; inferred
108    /// from the program's decode of it, not captured from the game).
109    pub component: u8,
110}
111/// The lanes `first..first + count` of an input that hold one value of the drawn instance ([`InputRole::Instance`]).
112#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
113pub struct InstanceLanes {
114    /// The value.
115    pub value: InstanceValue,
116    /// The first lane (0 for x).
117    pub first: u8,
118    /// How many lanes, from `first`, it fills: the value's own components, from its first.
119    pub count: u8,
120}
121/// A value of the drawn instance an instance stream carries ([`InputRole::Instance`]). The program places a vertex at
122/// `translation + scale * (axis 0, axis 1, axis 2) * p` for the position `p` it takes from the mesh.
123#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
124#[serde(rename_all = "snake_case")]
125#[non_exhaustive]
126pub enum InstanceValue {
127    /// The instance's translation in world space (three components).
128    Translation,
129    /// The instance's uniform scale (one component).
130    Scale,
131    /// Axis `n` of the instance's rotation (0 x, 1 y, 2 z) in world space, a unit vector (three components).
132    Axis(u8),
133    /// How far the instance's geometry is blended from the coarser level of detail the mesh also stores toward its own
134    /// position (one component): 1 draws the mesh's own position, 0 the coarser one.
135    LodBlend,
136}
137/// A camera or view-wide value.
138#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
139#[serde(rename_all = "snake_case")]
140#[non_exhaustive]
141pub enum ViewParameter {
142    /// The camera's position in world space (xyz).
143    CameraPosition,
144    /// The world-to-view matrix.
145    View,
146    /// The view-to-clip projection matrix.
147    Projection,
148    /// The world-to-clip matrix (view times projection).
149    ViewProjection,
150    /// The view-projection of a world with the camera at its origin: a program multiplies a world position minus the
151    /// camera's ([`ViewParameter::CameraPosition`]) by it, so it is [`ViewParameter::ViewProjection`] without the camera's
152    /// translation. The program takes the camera position from its own constant, so the two roles are bound together; a
153    /// consumer that feeds the plain matrix here puts the geometry in the wrong place.
154    CameraRelativeViewProjection,
155    /// The view-to-world matrix.
156    InverseView,
157    /// The clip-to-view matrix.
158    InverseProjection,
159    /// The clip-to-world matrix.
160    InverseViewProjection,
161    /// The render target's size in pixels as two values: the width, then the height. A source that packs more into
162    /// the register (the reciprocals beside the size, in either order) binds each pair by its own role over its own
163    /// lanes.
164    ViewportSize,
165    /// The reciprocals of the render target's size in pixels as two values: `1 / width`, then `1 / height`, the
166    /// uv-space step of a whole texel.
167    InverseViewportSize,
168    /// The distance from the camera to the near clip plane, along the view axis, in world units (one value).
169    NearPlane,
170    /// The distance from the camera to the far clip plane, along the view axis, in world units (one value).
171    FarPlane,
172    /// `1 / far plane`: the reciprocal of [`ViewParameter::FarPlane`] (one value).
173    InverseFarPlane,
174    /// The sign the projection puts on up (one value): +1 when a point above the view axis, in view space, has a positive
175    /// clip-space y, -1 when the projection negates y (a render into a texture, on a source whose texture origin is not
176    /// the screen's). A source that stores the opposite convention maps its value onto this one.
177    ProjectionFlip,
178    /// The previous frame's view-projection matrix (what a motion vector is measured against).
179    PreviousViewProjection,
180    /// The part of the render target the view draws into, per axis: its drawn size over the target's size, two values. A
181    /// renderer that lowers its resolution from frame to frame draws into that part of a target of the full size, so a
182    /// program finds its texture coordinate on the screen as the pixel position times
183    /// [`ViewParameter::InverseViewportSize`] over this; a view that fills its target has 1, 1.
184    ResolutionScale,
185    /// Whether the view's projection is a perspective one, as an unsigned 32-bit integer word: 1 for a perspective
186    /// projection (clip w is the depth along the view axis), 0 for an orthographic one.
187    Perspective,
188    /// The far clip plane's distance minus the near clip plane's ([`ViewParameter::FarPlane`] minus
189    /// [`ViewParameter::NearPlane`]), in world units (one value).
190    DepthRange,
191    /// The device depth a perspective projection gives a point infinitely far away: a point at depth `d` along the view
192    /// axis (world units) gets the device depth `offset + scale / d` ([`ViewParameter::DeviceDepthScale`] is `scale`),
193    /// so a program takes the depth back as `scale / (device depth - offset)`. One value.
194    DeviceDepthOffset,
195    /// The scale of a perspective projection's device depth over the depth along the view axis: `scale` in
196    /// [`ViewParameter::DeviceDepthOffset`]'s `offset + scale / d`. One value.
197    DeviceDepthScale,
198}
199/// A transform built from the drawn object's object-to-world matrix.
200#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
201#[serde(rename_all = "snake_case")]
202pub enum ObjectMatrix {
203    /// Object space to world space.
204    World,
205    /// Object space to view space.
206    WorldView,
207    /// Object space to clip space.
208    WorldViewProjection,
209}
210/// A value of the drawn object or instance.
211#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
212#[serde(rename_all = "snake_case")]
213#[non_exhaustive]
214pub enum ObjectParameter {
215    /// The object-to-world matrix.
216    World,
217    /// The world-to-object matrix.
218    InverseWorld,
219    /// The object-to-clip matrix.
220    WorldViewProjection,
221    /// The object-to-view matrix.
222    WorldView,
223    /// The view-to-object matrix: inverse(object-to-world) * inverse(world-to-view).
224    ViewToObject,
225    /// The transform as the previous frame had it: the object's previous pose, the previous view and projection (what a
226    /// motion vector is measured from).
227    PreviousFrame(ObjectMatrix),
228    /// The transform in the engine's camera-relative form (the world with the camera at its origin): a program computes
229    /// its clip position, and its previous-frame position, from this or from the plain transform, the one the draw
230    /// supplies (by the engine's own name for it; no capture checks the translation).
231    CameraRelative(ObjectMatrix),
232    /// The previous frame's transform, with the previous frame's camera at the origin of world space.
233    CameraRelativePreviousFrame(ObjectMatrix),
234    /// A colour the instance tints the surface with.
235    InstanceColour,
236    /// The scale (`xy`) and offset (`zw`) a model's texture coordinate set `n` is drawn with: the engine feeds one
237    /// value per model for each set, a scroll that animates some models (water, flags).
238    TextureScroll(u8),
239    /// The matrices of a skinned draw's joints, each a 3 x 4 transform (the object-to-world transform of the joint
240    /// the pose gives, in the engine's own palette order), its lines as the binding states ([`MatrixLines`]): what a
241    /// skinning program blends by the vertex's joint indices and weights. Where the program also reads
242    /// [`ObjectParameter::ClothCancelPalette`], a joint's transform is the product of the two; a consumer that has
243    /// each joint's whole object-to-world transform states it here and the identity there.
244    BonePalette,
245    /// The matrices of a second palette a vertex program takes the vertex through before
246    /// [`ObjectParameter::BonePalette`] (the engine's name for it is the cloth-cancel matrices): each a 3 x 4
247    /// transform held as the binding states ([`MatrixLines`]), chosen by the same index as the bone palette's (the
248    /// vertex's joint index plus the instance's matrix base, [`ObjectParameter::MatrixBase`]), so as long as it. The
249    /// vertex's transform is `BonePalette[j] * ClothCancelPalette[j]`: the position, and the normal and tangent by
250    /// the 3 x 3 part, go through this matrix first (blended over the vertex's joints by their weights where the
251    /// program blends joints, as Elden Ring's character programs that skin do; those that take one joint per vertex
252    /// do the same with that joint). How an engine splits a joint's transform between the two palettes is not
253    /// established: in a captured Elden Ring frame, 3,746 of the 12,230 recorded entries of skinned draws' palettes
254    /// were the identity, and 7,940 were not. A consumer that states each joint's whole transform in the bone palette
255    /// feeds the identity here, which leaves the first step without effect.
256    ClothCancelPalette,
257    /// The instance's environment data word (an unsigned integer the engine writes per instance; the layered programs
258    /// scale it by 1/255 into the alpha lane of a G-buffer target, and a program that reads a buffer of light sets takes
259    /// the set it names).
260    EnvironmentData,
261    /// The index of the instance's first matrix in the object-matrix palette, which the vertex program adds to the
262    /// index it takes (the joint indices of a skinned vertex, the instance's own for a rigid one).
263    MatrixBase,
264    /// The index of the draw's first record in the instance table ([`BufferTable::Instances`]), an unsigned integer word:
265    /// the vertex program adds the instance number to it and reads that record. It counts records of the instance table,
266    /// where [`ObjectParameter::MatrixBase`] counts matrices of the palette. A draw of one instance that starts the table
267    /// has 0.
268    InstanceBase,
269    /// Whether the draw simulates cloth: a nonzero word where the vertex program takes the cloth's matrix instead of the
270    /// palette's. A consumer that draws a static or rest pose states 0, which runs the palette arm.
271    ClothSimulation,
272    /// The instance's user clip plane (the vertex program's clip distance against it).
273    ClipPlane,
274    /// Row `n` (0 or 1) of an animated affine transform of the first texture coordinate set, three values each:
275    /// `u' = dot(row 0, (u, v, 1))`, `v' = dot(row 1, (u, v, 1))`. Some models animate their textures this way.
276    TextureTransformRow(u8),
277    /// Where along the rows of a palette texture the instance's tint selects (x): the v the program samples the
278    /// palette at, the u being a vertex colour.
279    PaletteCoordinate,
280    /// Whether the draw fades by the camera's distance, dithered (a flag the engine sets per draw): a program that reads it
281    /// keeps a fragment where `distance * CameraFadeInverseRange - CameraFadeEnd` is not below the dither noise.
282    CameraFadeEnabled,
283    /// The camera fade's end term, subtracted from the distance scaled by [`ObjectParameter::CameraFadeInverseRange`].
284    CameraFadeEnd,
285    /// The camera fade's inverse range: the factor the fragment's distance from the camera is scaled by.
286    CameraFadeInverseRange,
287    /// Whether the instance is drawn with dithered transparency (a per-draw flag): a program that reads it, with
288    /// [`ObjectParameter::DitherEnabled`] set, discards a fragment by the dither noise against the amount the instance record
289    /// carries ([`ObjectParameter::Fade`]).
290    DitherAlphaEnabled,
291    /// The second per-model flag of the dithered transparency, set together with [`ObjectParameter::DitherAlphaEnabled`]
292    /// (a program reads both, and fades only where both are on).
293    DitherEnabled,
294    /// The factor the engine fades an instance's visibility by (one value): 1 where it is fully shown, 0 where it is gone. A
295    /// program applies it where its pass keeps the instance's visibility: multiplied into the alpha it writes or tests (an
296    /// alpha test, also one steered through the depth output), stored as the alpha of the image a pass writes, as the weight
297    /// of the surface against the scene behind it where the program blends itself, and, in a pass that has no alpha, into the
298    /// emission it writes, or, under the dithered transparency's flags ([`ObjectParameter::DitherAlphaEnabled`]), as the
299    /// share of fragments a dithered test keeps (a fragment survives where the dither noise less one half plus this factor
300    /// reaches one half). A consumer that draws the instance as fully shown feeds 1. The camera fade and the dither flags
301    /// are other mechanisms: they switch the discard by noise on, they do not carry the factor.
302    Fade,
303    /// Whether the wind leaves the instance's vertices still (one value): above zero, the program places each vertex where
304    /// its rest position and the instance put it; at zero or below, it moves it by the wind state the program's other
305    /// wind constants carry. A consumer that draws the vegetation still states this and says so; one that animates it
306    /// needs those constants as well.
307    WindOff,
308}
309/// A value of the frame being drawn.
310#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
311#[serde(rename_all = "snake_case")]
312#[non_exhaustive]
313pub enum FrameParameter {
314    /// Seconds since a start the game defines.
315    Time,
316    /// The hour of the day the scene shows, one value, 0 to 24: the world's [`crate::TimeOfDay`]. A program of a source
317    /// that keys values by the hour reads it (Elden Ring's `FC_TimeOfDay`, which its sky programs take modulo 24 to blend
318    /// their material's colours by the hour). A world that states no hour gives such a program no value: a consumer
319    /// refuses the draw rather than show one hour as another.
320    TimeOfDay,
321    /// The pre-exposure: the factor the renderer multiplies the radiance it writes into the frame's light targets by (a
322    /// light's contribution, the ambient, a material's emission), so that the targets hold radiance times this factor; 1
323    /// where a renderer does not pre-expose. What the frame's exposure and tone stages do with it is theirs: they are passes
324    /// of their own, with their own bindings. A program that multiplies its emission by this value writes pre-exposed
325    /// radiance; a consumer that lights in unscaled radiance and exposes once at the end gives it 1.
326    PreExposure,
327    /// One over [`FrameParameter::PreExposure`]: the factor a program multiplies a value it reads back from the frame's
328    /// light targets by to take the pre-exposure off it. One value; 1 where the pre-exposure is 1.
329    InversePreExposure,
330    /// The frame's exposure: the factor its exposure stage multiplies the scene's radiance by before the tone curve (a
331    /// renderer's adapted exposure). One value; 1 where the frame is not exposed.
332    Exposure,
333    /// One over [`FrameParameter::Exposure`]: a program that multiplies a colour by it puts that colour on the screen at
334    /// the same brightness whatever the exposure. One value.
335    InverseExposure,
336    /// An index into a palette of colours a draw selects from.
337    ColourSlot,
338    /// The direction the dominant directional light travels (xyz): a surface is lit by `saturate(dot(normal, -xyz))`.
339    LightDirection,
340    /// The unit direction from the scene towards the dominant directional light (xyz): the opposite of
341    /// [`FrameParameter::LightDirection`], so a surface is lit by `saturate(dot(normal, xyz))`.
342    DirectionToLight,
343    /// The colour (rgb) of that light as the source's programs read it: the scene's sun's (`core.environment`'s)
344    /// `color` times its `intensity` over the world's [`crate::DirectionalLightUnit::lux_per_unit`], the value the
345    /// source stored. A world with no sun or no such unit gives a consumer no value of it.
346    LightColour,
347    /// The colour (rgb) of that light's highlights as the source's programs read it: as [`FrameParameter::LightColour`],
348    /// of the sun's `specular` colour and intensity where it states them ([`crate::LightSpecular`]), of its own
349    /// otherwise.
350    LightSpecularColour,
351    /// The directional ambient light as four values: its colour (xyz: a lighting program adds each channel to the same
352    /// channel of the lit colour, scaled by how much the surface faces an ambient direction it also reads), and a w that
353    /// scales the ambient terms a vertex carries, growing from 1 (no change) with distance.
354    DirectionalAmbientColour,
355    /// How many records the frame's light table of this kind holds ([`BufferSource::Lights`]): one unsigned 32-bit
356    /// integer, the number of lights the consumer packed ([`super::PackedLights::lights`]).
357    LightCount(super::LightTableKind),
358    /// The side of a light-list tile in pixels ([`BufferSource::LightLists`]): one unsigned 32-bit integer, the size the
359    /// consumer packed its lists for.
360    LightTileSize,
361    /// How many light-list tiles a row of the screen holds: one unsigned 32-bit integer, the render target's width over
362    /// [`FrameParameter::LightTileSize`], rounded up.
363    LightTilesPerRow,
364    /// The size of the frame's local-light shadow atlas in texels as two values, its width then its height
365    /// ([`crate::render_graph::RenderGraph::local_shadow_atlas`]).
366    LocalShadowAtlasSize,
367    /// The reciprocals of [`FrameParameter::LocalShadowAtlasSize`]: `1 / width`, then `1 / height`, the step of one of
368    /// its texels in its texture coordinates.
369    InverseLocalShadowAtlasSize,
370}
371/// A value of the item one run of a repeated step is for ([`crate::render_graph::Items`]).
372#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
373#[serde(rename_all = "snake_case")]
374pub enum ItemParameter {
375    /// The record of the shadowed light the run is for ([`crate::render_graph::Items::ShadowedLights`]): the bytes the
376    /// frame's light table of the step's kind packs for it with its shadow, the whole record, so the constant's size is
377    /// the table's stride.
378    LightRecord,
379}
380/// What a constant of a buffer stands for.
381#[derive(Clone, Debug, PartialEq, Serialize)]
382#[serde(rename_all = "snake_case")]
383pub enum ConstantRole {
384    /// The material's own parameter, as stored: the 32-bit words the constant takes (bit patterns, so an integer
385    /// parameter and a float one are both exact).
386    Material(Vec<u32>),
387    /// A camera or view-wide value.
388    View(ViewParameter),
389    /// A value of the drawn object.
390    Object(ObjectParameter),
391    /// A value of the frame.
392    Frame(FrameParameter),
393    /// A value of the item the run of a repeated step is for ([`crate::render_graph::Items`]).
394    Item(ItemParameter),
395    /// A reference confirmed by the program's discard graph. `compared` states whether a portable cutoff on output
396    /// alpha is the same test or an approximation. A baking consumer freezes the reference below the alpha's range, and must
397    /// also neutralize any stated dither perturbation; the draw uses the material's original `value`.
398    AlphaReference {
399        /// The reference the discard compares against.
400        value: f32,
401        /// What the reference is compared with.
402        compared: AlphaComparison,
403    },
404    /// Not resolved to a role, and why.
405    Unknown {
406        /// What was examined and why no role follows.
407        why: String,
408    },
409    /// A value of the engine's that no role of core states, set by the source's own policy: the 32-bit words the
410    /// constant takes, and why (as [`super::TextureSource::Default`] sets a texture slot the engine fills). Not the
411    /// material's: a consumer writes the words as they are.
412    Default {
413        /// The words, as [`ConstantRole::Material`]'s.
414        words: Vec<u32>,
415        /// The policy and the evidence for its words.
416        why: String,
417    },
418}
419/// One constant: the bytes `offset..offset + size` of a constant buffer resource of the shader, named when the
420/// source names it (a stripped program has fields by offset only), and what it stands for.
421#[derive(Clone, Debug, PartialEq, Serialize)]
422pub struct ConstantBinding {
423    /// The constant buffer resource holding the constant.
424    pub resource: ResourceId,
425    /// Byte offset in the buffer.
426    pub offset: u32,
427    /// Bytes.
428    pub size: u32,
429    /// The constant's name, `None` for a stripped program.
430    pub name: Option<String>,
431    /// What the constant stands for.
432    pub role: ConstantRole,
433    /// For a matrix role ([`ConstantRole::is_matrix`]), how its 16-byte lines hold the role's matrix
434    /// ([`MatrixLines`]); `None` for every other role.
435    #[serde(skip_serializing_if = "Option::is_none")]
436    pub lines: Option<MatrixLines>,
437}
438impl ConstantRole {
439    /// Whether the role is a matrix, whose binding states its lines ([`ConstantBinding::lines`]): the view's and the
440    /// object's transforms, their inverses and previous-frame and camera-relative forms, and the palettes.
441    pub fn is_matrix(&self) -> bool {
442        match self {
443            ConstantRole::View(v) => matches!(
444                v,
445                ViewParameter::View
446                    | ViewParameter::Projection
447                    | ViewParameter::ViewProjection
448                    | ViewParameter::CameraRelativeViewProjection
449                    | ViewParameter::InverseView
450                    | ViewParameter::InverseProjection
451                    | ViewParameter::InverseViewProjection
452                    | ViewParameter::PreviousViewProjection
453            ),
454            ConstantRole::Object(o) => matches!(
455                o,
456                ObjectParameter::World
457                    | ObjectParameter::InverseWorld
458                    | ObjectParameter::WorldViewProjection
459                    | ObjectParameter::WorldView
460                    | ObjectParameter::ViewToObject
461                    | ObjectParameter::PreviousFrame(_)
462                    | ObjectParameter::CameraRelative(_)
463                    | ObjectParameter::CameraRelativePreviousFrame(_)
464                    | ObjectParameter::BonePalette
465                    | ObjectParameter::ClothCancelPalette
466            ),
467            ConstantRole::Material(_)
468            | ConstantRole::Frame(_)
469            | ConstantRole::Item(_)
470            | ConstantRole::AlphaReference { .. }
471            | ConstantRole::Unknown { .. }
472            | ConstantRole::Default { .. } => false,
473        }
474    }
475}
476/// An image of the frame being drawn that a program reads: the consumer supplies it from its own frame, as it supplies a view or
477/// frame constant.
478#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
479#[serde(rename_all = "snake_case")]
480#[non_exhaustive]
481pub enum FrameTexture {
482    /// The depth the frame's depth pass wrote, the hardware depth in `x`: a program that reads it linearises it with the scene
483    /// constants' depth parameters (soft edges of fog, foam and water against the geometry behind).
484    SceneDepth,
485    /// The colour image of what the frame has drawn so far behind the surface (the opaque scene, or the screen as it
486    /// stands): what a refracting or see-through surface samples.
487    SceneColour,
488    /// The depth image rendered from a shadow-casting light (the main light's, its cascades side by side where it has
489    /// them): a program compares a surface's depth in that light's space with it.
490    ShadowMap,
491    /// The shadow term of the main light gathered per screen pixel: a program samples it at its own screen position, 1
492    /// where the light reaches and 0 where it is shadowed.
493    ScreenSpaceShadows,
494    /// The ambient occlusion the frame computed per screen pixel (1 unoccluded).
495    AmbientOcclusion,
496    /// The cube image of the environment around the surface as the nearest reflection probe, or the sky where there is
497    /// none, captured: what a reflection samples along the reflected view direction.
498    ReflectionProbe,
499    /// The volume image of the light probes' lighting around the surface (a 3D image of spherical-harmonic
500    /// coefficients the surface samples at its position).
501    LightProbeVolume,
502    /// Image `n` of the decal buffer the frame's decal step draws before the surfaces are lit (Unity's DBuffer: 0 the
503    /// decals' albedo, 1 their normal, 2 their metallic, occlusion and smoothness, each with the decals' coverage in
504    /// alpha): a lit draw reads it at its own screen position and blends the decals into its surface.
505    DecalBuffer(u8),
506}
507impl FrameTexture {
508    /// The dimension of the image: a probe's is a cube, the light probes' a volume, every other a plane.
509    pub fn dim(self) -> iridia_interface::TextureDim {
510        match self {
511            Self::ReflectionProbe => iridia_interface::TextureDim::Cube,
512            Self::LightProbeVolume => iridia_interface::TextureDim::D3,
513            Self::SceneDepth
514            | Self::SceneColour
515            | Self::ShadowMap
516            | Self::ScreenSpaceShadows
517            | Self::AmbientOcclusion
518            | Self::DecalBuffer(_) => iridia_interface::TextureDim::D2,
519        }
520    }
521}
522/// One buffer slot: a buffer resource of the shader (a structured, raw or typed buffer a program indexes at run time) and
523/// what the source binds to it.
524#[derive(Clone, Debug, PartialEq, Serialize)]
525pub struct BufferBinding {
526    /// The buffer resource of the shader.
527    pub resource: ResourceId,
528    /// What the source binds to it.
529    pub source: BufferSource,
530}
531/// What a buffer slot holds.
532#[derive(Clone, Debug, PartialEq, Serialize)]
533#[serde(rename_all = "snake_case")]
534pub enum BufferSource {
535    /// A table of the draw's own data, by what it holds: the consumer fills it from the draw it makes.
536    Table(BufferTable),
537    /// Nothing is known of the contents and the source says why: a consumer states the stand-in it uses, and never
538    /// reads the buffer as zeros without saying so (as a texture with no known source, or an unknown
539    /// constant).
540    Unknown {
541        /// Why nothing is known of the contents.
542        why: String,
543    },
544    /// One of the frame's own buffers: only in the binding of a step of the frame's `core.render_graph`
545    /// ([`crate::render_graph::RenderGraph::buffers`]), whose buffers these are. A material's binding never states one.
546    Graph(crate::render_graph::BufferId),
547    /// The scene's lights of one kind, one record each in the table's layout, which the consumer packs from the world's
548    /// `core.light`s for the frame's view ([`super::LightTable::pack`]).
549    Lights(super::LightTable),
550    /// One of the two buffers of the frame's per-tile light lists, which say which lights a program visits at a pixel;
551    /// the consumer packs both with [`super::pack_light_lists`] from the lights it packed into the stage's light tables.
552    LightLists(super::LightListPart),
553}
554/// A table of the draw's data a program reads from a buffer.
555#[derive(Clone, Debug, PartialEq, Serialize)]
556#[serde(rename_all = "snake_case")]
557#[non_exhaustive]
558pub enum BufferTable {
559    /// The matrices of the draw: the same content as [`ObjectParameter::BonePalette`] (the object-to-world transforms,
560    /// in the engine's own order, each `rows` rows of four floats: three for an affine transform without its last row,
561    /// four for a full matrix) in a buffer instead of constants, and with that role's row convention. They lie one
562    /// matrix after another, so element `i` of the buffer is row `i % rows` of matrix `i / rows`; the elements are
563    /// four floats, 16 bytes.
564    ObjectMatrices {
565        /// Rows of four floats per matrix: 3 or 4.
566        rows: u8,
567    },
568    /// [`BufferTable::ObjectMatrices`] with one header element before each matrix: the same rows and row convention, `rows`
569    /// rows of four floats after a header element of four words, so element `i` of the buffer is the header of matrix
570    /// `i / (rows + 1)` when `i % (rows + 1)` is 0 and row `i % (rows + 1) - 1` of it otherwise.
571    ///
572    /// The program reads flags from the header's first word: its bit 0 with a 16-bit offset in bits 1 to 16, and its bit 18,
573    /// each put the rows later by that many elements. A consumer writes every header as zero, which puts the rows directly
574    /// after it. That the zero header is that layout is what a program run against such a table gave (identity rows after
575    /// a zero header gave the position back), not something the engine documents.
576    HeaderedMatrices {
577        /// Rows of four floats after each header element: 3 or 4.
578        rows: u8,
579    },
580    /// The block of an instance whose program skins, in a table of elements of four floats each (the shape every one of
581    /// the five skinned vertex programs of one game's hat materials reads, measured by running each against tables filled to
582    /// tell the parts apart):
583    ///
584    /// - a header element, as [`BufferTable::HeaderedMatrices`] (the instance record names it);
585    /// - the first prefix element, `X`, `1 + offset` elements after the header, where `offset` is the header's bit 0 and
586    ///   16-bit offset in bits 1 to 16 (zero when the bit is clear): its meaning is `first`;
587    /// - the other prefix elements, `X + 1` to `X + prefix - 1`: none of the five programs reads them. A program that
588    ///   reads one is not described by this role (a consumer fills them with zeros);
589    /// - then the joints' matrices, `joint_stride` elements apart, joint `j`'s `rows` rows starting at element
590    ///   `X + prefix + bit18 + joint_stride * j`, where `bit18` is the header's bit 18 (the rows are one element later
591    ///   when it is set; `X` is not).
592    ///
593    /// The position of a vertex is the sum, over its influences, of the weight times the joint's matrix applied to the
594    /// position (the rows as [`ObjectParameter::BonePalette`] has them), then `first`. A consumer writes a zero header
595    /// (no offset, no bit 18), which puts `X` directly after the header and the first joint's rows `prefix` elements
596    /// after `X`.
597    SkinnedMatrices {
598        /// Rows of one joint's matrix: three (an affine transform without its last row) or four.
599        rows: u8,
600        /// Elements from one joint's first row to the next joint's.
601        joint_stride: u8,
602        /// Elements from `X` to the first joint's first row when the header's bit 18 is clear.
603        prefix: u8,
604        /// What `X` holds.
605        first: SkinPrefix,
606    },
607    /// One record per instance drawn, `stride` 32-bit words each, in instance order: word `matrix_start` of a record is
608    /// the index, in elements of the table `matrices` (a [`BufferTable::ObjectMatrices`] or a
609    /// [`BufferTable::HeaderedMatrices`] or [`BufferTable::SkinnedMatrices`] of the same stage), of the instance's first element:
610    /// its first row, or its header for a headered table. The program adds the indices of the matrices it takes (a joint's, a model's own) to it.
611    Instances {
612        /// 32-bit words in each record.
613        stride: u8,
614        /// Index of the word of a record that holds the index of the instance's first matrix element.
615        matrix_start: u8,
616        /// The matrix table (`ObjectMatrices`, `HeaderedMatrices` or `SkinnedMatrices`) the indices point into.
617        matrices: ResourceId,
618    },
619}
620/// What the first prefix element of a [`BufferTable::SkinnedMatrices`] holds.
621#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
622#[serde(rename_all = "snake_case")]
623#[non_exhaustive]
624pub enum SkinPrefix {
625    /// The xyz lanes are added, once and unweighted, to the position the weighted sum of the joints' transforms gives
626    /// (so a vertex at `p` with one influence of weight `w` ends at `w * (M p) + xyz`); the w lane is not read.
627    Translation,
628}
629/// What an [`OutputRole::Occlusion`] occludes.
630#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
631#[serde(rename_all = "snake_case")]
632#[non_exhaustive]
633pub enum Occluded {
634    /// Every light that reaches the surface from outside it: the one occlusion a source has.
635    AllLight,
636    /// The ambient light of the environment itself (the sky and what it scatters): the term a lighting pass scales that
637    /// ambient by, where the source keeps another occlusion for the light of placed artificial sources.
638    NaturalAmbient,
639    /// The ambient light of placed artificial sources (interior and exterior ambient volumes), apart from the
640    /// environment's own ([`Occluded::NaturalAmbient`]).
641    ArtificialAmbient,
642}
643/// What lanes of an output stand for.
644#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
645#[serde(rename_all = "snake_case")]
646#[non_exhaustive]
647pub enum OutputRole {
648    /// The surface's albedo (base colour), linear.
649    Albedo,
650    /// The surface normal, decoded by `encoding` into `space`.
651    Normal {
652        /// How the normal is stored in the lanes.
653        encoding: NormalEncoding,
654        /// The space the normal is in.
655        space: NormalSpace,
656    },
657    /// Smoothness, 1 - perceptual roughness: the value decoded by `encoding`.
658    Gloss {
659        /// How the gloss is stored.
660        encoding: ScalarEncoding,
661    },
662    /// The perceptual roughness, stored as is.
663    Roughness,
664    /// The metalness (0 to 1).
665    Metallic,
666    /// The factor the light `of` is scaled by where the surface is shadowed from it (1 unoccluded, 0 fully): the value
667    /// decoded by `encoding`, as the lighting pass multiplies the light by it.
668    Occlusion {
669        /// Which light the occlusion applies to.
670        of: Occluded,
671        /// How the factor is stored.
672        encoding: ScalarEncoding,
673    },
674    /// How much of the light a surface reflects as a highlight is blocked where the surface faces away from the
675    /// environment (1 unblocked): the term a lighting pass multiplies its specular by, apart from the diffuse
676    /// [`OutputRole::Occlusion`].
677    SpecularOcclusion,
678    /// How much light passes through the surface (a leaf, a petal): a lighting pass tints the albedo by it and adds it
679    /// to the lit colour, 0 for an opaque surface.
680    Translucency,
681    /// Specular reflectance at normal incidence (F0), the value decoded by `encoding`.
682    Reflectance {
683        /// How the reflectance is stored.
684        encoding: ScalarEncoding,
685    },
686    /// The strength of the specular term, how much light the surface reflects as a highlight (0 to 1 once decoded).
687    SpecularIntensity {
688        /// How the strength is stored.
689        encoding: ScalarEncoding,
690    },
691    /// How tight the specular highlight is: the material's falloff or exponent as the source's lighting uses it
692    /// (what lighting derives from it, a Blinn-Phong exponent for instance, is the source's).
693    SpecularExponent {
694        /// How the exponent is stored.
695        encoding: ScalarEncoding,
696    },
697    /// The source's Fresnel term for the surface, as the source stores it (how the lighting turns it into a
698    /// reflectance at grazing angles is the source's).
699    Fresnel,
700    /// The screen-space displacement, in pixels, from the fragment's position in the frame being drawn to its
701    /// position in the previous frame (x right, y down).
702    MotionVector,
703    /// The light the surface gives off: a scalar in one lane that scales the albedo (the lighting pass adds the albedo
704    /// times it to the lit colour) or a colour in three, the value decoded by `encoding`.
705    Emission {
706        /// How the emission is stored.
707        encoding: ScalarEncoding,
708    },
709    /// The finished colour of a forward pass.
710    Colour,
711    /// Whether the fragment is kept: a value a discard of the program tests (the lane's written value is among what the
712    /// discard's conditions are made of), so the surface is cut where the test fails. Not a fraction.
713    Coverage,
714    /// How opaque the surface is (1 opaque) as the target's blend takes it: the alpha lane of a target whose stated
715    /// blend weighs the colour by the source alpha.
716    Opacity,
717    /// Lanes the program writes that no role is established for, with why: what the source's evidence shows of them
718    /// (what reads them, or that nothing examined does). Never empty.
719    Unknown {
720        /// What the source's evidence shows of the lanes.
721        why: String,
722    },
723}
724impl OutputRole {
725    /// [`OutputRole::Unknown`] with `why`.
726    pub fn unknown(why: impl Into<String>) -> OutputRole {
727        OutputRole::Unknown { why: why.into() }
728    }
729
730    /// The encoding a role's value is stored in, for the roles that state one.
731    pub fn scalar_encoding(&self) -> Option<ScalarEncoding> {
732        match *self {
733            OutputRole::Gloss { encoding }
734            | OutputRole::Occlusion { encoding, .. }
735            | OutputRole::Reflectance { encoding }
736            | OutputRole::SpecularIntensity { encoding }
737            | OutputRole::SpecularExponent { encoding }
738            | OutputRole::Emission { encoding } => Some(encoding),
739            _ => None,
740        }
741    }
742}
743/// Lanes of one output varying and their role: an output's lanes may carry different things (albedo in rgb and
744/// occlusion in a), as several bindings of one label.
745#[derive(Clone, Debug, PartialEq, Serialize)]
746pub struct OutputBinding {
747    /// The output varying the lanes belong to.
748    pub label: Label,
749    /// Lane mask: bit k is lane k of the output's value.
750    pub lanes: u8,
751    /// What the lanes stand for.
752    pub role: OutputRole,
753}
754/// A stage's input binding: the role of an input varying the shader declares.
755#[derive(Clone, Debug, PartialEq, Serialize)]
756pub struct InputBinding {
757    /// The input varying.
758    pub label: Label,
759    /// What the input stands for.
760    pub role: InputRole,
761}
762/// What picks one of several stages of one kind in a binding: the mesh-side field a consumer reads, named by the
763/// dialect's component and field that holds it (`<component>.<field>`), and the value that picks this stage. A
764/// consumer draws a mesh with the stage whose key names a field it has and whose value that field equals; a mesh whose
765/// value matches no stage is refused by name, not drawn with a first or default stage.
766#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
767pub struct VariantKey {
768    /// The component and field the mesh carries the key in, as `component.field` (`<dialect>.<component>.<field>`), or
769    /// the name the source gives its keys where no mesh field holds them (a name in the source's own terms).
770    pub field: String,
771    /// The value of the stage's key, compared for equality with the mesh's field.
772    pub value: u32,
773    /// What the key stands for, in the source's terms. A description only: it never identifies a variant (a binding's
774    /// check, and a consumer picking a stage, go by `field` and `value`) and may be empty.
775    #[serde(skip_serializing_if = "Vec::is_empty")]
776    pub parts: Vec<KeyPart>,
777}
778/// One named number of what a [`VariantKey`] stands for (a program's place in its row, a pixel output count, a
779/// permutation id). It describes and never identifies.
780#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
781pub struct KeyPart {
782    /// The part's name.
783    pub name: String,
784    /// The part's value.
785    pub value: u64,
786}
787
788/// States the lines ([`ConstantBinding::lines`]) of each matrix role among `constants`, as `lines_at` reads them from the
789/// program (`iridia::matrix::lines_at` over the program's value graph); every other role is left alone. Returns how many
790/// are [`MatrixLines::Unshown`].
791pub fn state_lines(
792    constants: &mut [ConstantBinding],
793    mut lines_at: impl FnMut(&ConstantBinding) -> MatrixLines,
794) -> usize {
795    let mut unshown = 0usize;
796    for c in constants.iter_mut().filter(|c| c.role.is_matrix()) {
797        let lines = lines_at(c);
798        if matches!(lines, MatrixLines::Unshown(_)) {
799            unshown = unshown.saturating_add(1);
800        }
801        c.lines = Some(lines);
802    }
803    unshown
804}
805
806/// The lanes [`bind_output_uses`] bound, for a log.
807#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
808pub struct OutputUses {
809    /// Lanes bound as `OutputRole::Coverage`.
810    pub coverage: u32,
811    /// Lanes bound as `OutputRole::Opacity`.
812    pub opacity: u32,
813}
814
815/// Bind the Unknown lanes of a pixel stage's `outputs` that what its program shows of them (`uses`, from
816/// `iridia::output_uses::output_uses`) gives a use for, by the output's label:
817///
818/// - [`OutputRole::Opacity`], a lane the target's stated blend weighs the colour by ([`OutputUse::blend_weight`]: the
819///   alpha lane of a target blended by the source alpha);
820/// - else [`OutputRole::Coverage`], a lane a discard tests ([`OutputUse::discard_tested`]).
821///
822/// A lane with a role keeps it, and an Unknown binding keeps its why on the lanes not bound. A caller that states roles
823/// for the outputs of a program calls it after stating its own; nothing is read from a program's or a target's name.
824pub fn bind_output_uses(outputs: &mut Vec<OutputBinding>, uses: &[OutputUse]) -> OutputUses {
825    let mut counts = OutputUses::default();
826    let mut bound = Vec::with_capacity(outputs.len());
827    for o in outputs.drain(..) {
828        let OutputRole::Unknown { why } = &o.role else {
829            bound.push(o);
830            continue;
831        };
832        let Some(seen) = uses.iter().find(|u| u.label == o.label) else {
833            bound.push(o);
834            continue;
835        };
836        let opacity = o.lanes & seen.blend_weight;
837        let coverage = o.lanes & seen.discard_tested & !opacity;
838        let rest = o.lanes & !(opacity | coverage);
839        if rest != 0 {
840            bound.push(OutputBinding {
841                label: o.label.clone(),
842                lanes: rest,
843                role: OutputRole::unknown(why.clone()),
844            });
845        }
846        for (lanes, role) in [(coverage, OutputRole::Coverage), (opacity, OutputRole::Opacity)] {
847            if lanes != 0 {
848                bound.push(OutputBinding {
849                    label: o.label.clone(),
850                    lanes,
851                    role,
852                });
853            }
854        }
855        counts.coverage = counts.coverage.saturating_add(coverage.count_ones());
856        counts.opacity = counts.opacity.saturating_add(opacity.count_ones());
857    }
858    *outputs = bound;
859    counts
860}
861
862#[cfg(test)]
863#[allow(clippy::unwrap_used, reason = "hand-worked values")]
864mod tests {
865    use super::*;
866
867    /// The camera-relative view-projection is a role of its own: a source that states it is not read as the plain matrix.
868    #[test]
869    fn a_camera_relative_view_projection_is_not_the_plain_one() {
870        assert_ne!(
871            ConstantRole::View(ViewParameter::CameraRelativeViewProjection),
872            ConstantRole::View(ViewParameter::ViewProjection)
873        );
874        assert_eq!(
875            serde_json::to_value(ViewParameter::CameraRelativeViewProjection).unwrap(),
876            serde_json::json!("camera_relative_view_projection")
877        );
878    }
879
880    fn target(n: u32) -> Label {
881        Label {
882            name: "SV_Target".into(),
883            index: n,
884        }
885    }
886
887    fn seen(n: u32, blend_weight: u8, discard_tested: u8) -> OutputUse {
888        OutputUse {
889            label: target(n),
890            target: n,
891            blend_weight,
892            discard_tested,
893        }
894    }
895
896    /// A lane the blend weighs by is Opacity, over a discard's test of it; a tested lane is Coverage; a lane with a
897    /// role keeps it; an Unknown binding keeps its why on the lanes not bound; an output nothing is seen of is left.
898    #[test]
899    fn the_uses_seen_bind_the_unknown_lanes() {
900        let unknown = |n: u32, lanes: u8| OutputBinding {
901            label: target(n),
902            lanes,
903            role: OutputRole::unknown(format!("target {n}: no role")),
904        };
905        let mut outputs = vec![
906            OutputBinding {
907                label: target(0),
908                lanes: 0b0111,
909                role: OutputRole::Albedo,
910            },
911            unknown(0, 0b1000),
912            unknown(1, 0b1111),
913            unknown(2, 0b1000),
914            unknown(3, 0b1111),
915        ];
916        let uses = [
917            // the albedo's lanes are tested too: it keeps its role
918            seen(0, 0, 0b1111),
919            seen(1, 0b1000, 0b1001),
920            seen(2, 0, 0),
921        ];
922        let counts = bind_output_uses(&mut outputs, &uses);
923        let roles: Vec<_> = outputs
924            .iter()
925            .map(|o| (o.label.index, o.lanes, o.role.clone()))
926            .collect();
927        assert_eq!(
928            roles,
929            [
930                (0, 0b0111, OutputRole::Albedo),
931                (0, 0b1000, OutputRole::Coverage),
932                (1, 0b0110, OutputRole::unknown("target 1: no role")),
933                (1, 0b0001, OutputRole::Coverage),
934                (1, 0b1000, OutputRole::Opacity),
935                (2, 0b1000, OutputRole::unknown("target 2: no role")),
936                (3, 0b1111, OutputRole::unknown("target 3: no role")),
937            ]
938        );
939        assert_eq!(
940            counts,
941            OutputUses {
942                coverage: 2,
943                opacity: 1
944            }
945        );
946    }
947
948    /// Each matrix role is given the lines read for it, the others none; the unshown are counted.
949    #[test]
950    fn the_lines_of_each_matrix_role_are_stated() {
951        let constant = |offset: u32, role: ConstantRole| ConstantBinding {
952            resource: ResourceId(0),
953            offset,
954            size: 64,
955            name: None,
956            role,
957            lines: None,
958        };
959        let mut constants = vec![
960            constant(0, ConstantRole::View(ViewParameter::ViewProjection)),
961            constant(64, ConstantRole::Object(ObjectParameter::BonePalette)),
962            constant(128, ConstantRole::View(ViewParameter::CameraPosition)),
963            constant(144, ConstantRole::View(ViewParameter::InverseView)),
964        ];
965        let unshown = state_lines(&mut constants, |c| match c.offset {
966            0 => MatrixLines::Columns,
967            64 => MatrixLines::Rows,
968            _ => MatrixLines::Unshown(iridia_interface::Unshown::NotApplied),
969        });
970        assert_eq!(unshown, 1);
971        let lines: Vec<_> = constants.iter().map(|c| c.lines.clone()).collect();
972        assert_eq!(
973            lines,
974            [
975                Some(MatrixLines::Columns),
976                Some(MatrixLines::Rows),
977                None,
978                Some(MatrixLines::Unshown(iridia_interface::Unshown::NotApplied)),
979            ]
980        );
981    }
982}