Skip to main content

unity_formats/
program.rs

1//! A Shader's compiled programs, as a player stores them: per pass, each stage's program (vertex, fragment, geometry,
2//! hull, domain: the stages the pass has) with its variants (each a keyword
3//! set, a GPU program type and an entry of the compressed blob) and their parameters (constant buffers with each
4//! field's name and byte offset, texture and sampler bind slots), read from `m_ParsedForm`; and a variant's code from
5//! the platform's LZ4-compressed blob. What the bindings mean for a material is the caller's.
6//!
7//! Players store the variants in one of two layouts, told apart by their fields: from 2021.2, `m_PlayerSubPrograms`
8//! (lists of variants, keywords indexing the form's `m_KeywordNames`) with `m_CommonParameters` shared by them;
9//! before it, `m_SubPrograms` (one list, each variant with its hardware tier, its keywords as global and local indices
10//! into the pass's name table, and its own parameters). Struct fields (instancing's per-draw arrays) are read as
11//! [`StructField`]s.
12//!
13//! The blob of one platform is split into segments (`offsets`, `compressedLengths`, `decompressedLengths`, one
14//! entry per segment). Segment 0 starts with the entry table: a count, then per entry its offset, length and segment.
15//! An entry of a program holds Unity's program record: its version, GPU program type, then (from 2016.2) a header the
16//! reader skips, keyword strings (empty in current players: the parsed form names them), and the code as a byte array.
17
18use primase_foundations::compress::{Decompressor, Lz4};
19
20use crate::player::{ObjRef, Player};
21use crate::value::{Array, Value};
22use crate::{Result, invalid};
23
24/// A constant-buffer field: its name, byte offset in the buffer, element type (Unity's `ShaderParamType`: 0 float, 1
25/// int, 2 bool, 3 half, 4 short, 5 uint), shape (a vector of `columns` lanes, or a matrix of `rows` rows) and array
26/// size (0: not an array).
27#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
28pub struct Field {
29    /// The field's name.
30    pub name: String,
31    /// The field's byte offset in its buffer.
32    pub offset: u32,
33    /// Unity's `ShaderParamType` of the element.
34    pub kind: i64,
35    /// A vector or a matrix.
36    pub shape: Shape,
37    /// The array length; 0 when the field is not an array.
38    pub array_size: u32,
39}
40
41/// A field's shape.
42#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
43pub enum Shape {
44    /// A vector.
45    Vector {
46        /// Its lane count (the stored column count: 2 to 4).
47        columns: u32,
48    },
49    /// A matrix.
50    Matrix {
51        /// Its row count as stored.
52        rows: u32,
53    },
54}
55
56/// A constant-buffer field of a struct type (Unity's instancing arrays): its name, byte offset, array size (0: not
57/// an array), the struct's size in bytes and its members (offsets within the struct).
58#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
59pub struct StructField {
60    /// The field's name.
61    pub name: String,
62    /// The field's byte offset in its buffer.
63    pub offset: u32,
64    /// The array length; 0 when the field is not an array.
65    pub array_size: u32,
66    /// The struct's size in bytes.
67    pub size: u32,
68    /// The struct's members, their offsets within the struct.
69    pub members: Vec<Field>,
70}
71
72/// A constant buffer: its name, the bind slot it is bound to (`b<n>`; `None` when no binding names it), its size in
73/// bytes, its fields and its struct fields (by offset).
74#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
75pub struct ConstantBuffer {
76    /// The buffer's name.
77    pub name: String,
78    /// The bind slot (`b<n>`), when a binding names the buffer.
79    pub slot: Option<u32>,
80    /// The buffer's size in bytes.
81    pub size: u32,
82    /// The buffer's fields.
83    pub fields: Vec<Field>,
84    /// The buffer's struct fields, by offset.
85    #[serde(skip_serializing_if = "Vec::is_empty")]
86    pub structs: Vec<StructField>,
87}
88
89/// A texture parameter: its name, bind slot (`t<n>`), the sampler slot it samples with (`None`: none of its own) and
90/// dimension (Unity's `TextureDimension`: 2 a 2D texture, 3 3D, 4 a cube).
91#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
92pub struct TextureParam {
93    /// The texture's name.
94    pub name: String,
95    /// The bind slot (`t<n>`).
96    pub slot: u32,
97    /// The slot of the sampler it samples with; `None` when it has none of its own.
98    pub sampler: Option<u32>,
99    /// Unity's `TextureDimension`: 2 a 2D texture, 3 a 3D texture, 4 a cube.
100    pub dim: i64,
101}
102
103/// The parameters the variants of a program share.
104#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize)]
105pub struct Parameters {
106    /// The constant buffers.
107    pub constant_buffers: Vec<ConstantBuffer>,
108    /// The texture parameters.
109    pub textures: Vec<TextureParam>,
110    /// Sampler states: Unity's packed sampler value and its bind slot (`s<n>`).
111    pub samplers: Vec<(u32, u32)>,
112}
113
114/// One compiled variant: the blob entry holding its code, its keywords, its GPU program type (Unity's
115/// `ShaderGpuProgramType`: 17 a D3D11 pixel program before 2021.2, 18 an SM 5.0 one after) and its shader
116/// requirements mask; `group` is the index of the player sub-program list it is in (from 2021.2) or its hardware tier
117/// (before); `parameters` its own, where the player stores them per variant (before 2021.2), else `None`: the pass's.
118#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
119pub struct Variant {
120    /// The index of the player sub-program list the variant is in (2021.2 on), or its hardware tier (before).
121    pub group: usize,
122    /// The entry of the platform's blob that holds the variant's program record.
123    pub blob_index: u32,
124    /// The keywords the variant is compiled with.
125    pub keywords: Vec<String>,
126    /// Unity's `ShaderGpuProgramType`.
127    pub gpu_program_type: i64,
128    /// The shader requirements mask.
129    pub requirements: i64,
130    /// The blob entry holding this variant's own parameters, where the player stores them there (from 2021.2:
131    /// `m_ParameterBlobIndices`, parallel to the sub-program lists).
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub parameter_blob: Option<u32>,
134    /// The variant's own parameters, where the player stores them per variant (before 2021.2); `None`: the pass
135    /// program's.
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub parameters: Option<Parameters>,
138}
139
140/// One pass's program of one stage: its variants and the parameters common to them (empty where each variant has its
141/// own).
142#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize)]
143pub struct PassProgram {
144    /// The variants, in stored order.
145    pub variants: Vec<Variant>,
146    /// The parameters the variants share.
147    pub parameters: Parameters,
148}
149
150impl PassProgram {
151    /// The parameters variant `v` (of this pass) is compiled with.
152    pub fn parameters_of<'a>(&'a self, v: &'a Variant) -> &'a Parameters {
153        v.parameters.as_ref().unwrap_or(&self.parameters)
154    }
155}
156
157/// The stages Unity stores a pass's programs for (`progVertex`, `progFragment`, `progGeometry`, `progHull`,
158/// `progDomain`), in that order.
159#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize)]
160pub enum Stage {
161    /// `progVertex`.
162    Vertex,
163    /// `progFragment`.
164    Fragment,
165    /// `progGeometry`.
166    Geometry,
167    /// `progHull`.
168    Hull,
169    /// `progDomain`.
170    Domain,
171}
172
173impl Stage {
174    /// Every stage, in Unity's order.
175    pub const ALL: [Stage; 5] = [
176        Stage::Vertex,
177        Stage::Fragment,
178        Stage::Geometry,
179        Stage::Hull,
180        Stage::Domain,
181    ];
182
183    /// The pass field Unity stores the stage's program under.
184    fn field(self) -> &'static str {
185        match self {
186            Stage::Vertex => "progVertex",
187            Stage::Fragment => "progFragment",
188            Stage::Geometry => "progGeometry",
189            Stage::Hull => "progHull",
190            Stage::Domain => "progDomain",
191        }
192    }
193}
194
195/// One pass's programs, a [`PassProgram`] per stage (empty where the pass stores none for it).
196#[derive(Clone, Debug, Default, PartialEq, Eq)]
197pub struct PassStages {
198    /// The vertex program.
199    pub vertex: PassProgram,
200    /// The fragment program.
201    pub fragment: PassProgram,
202    /// The geometry program.
203    pub geometry: PassProgram,
204    /// The hull program.
205    pub hull: PassProgram,
206    /// The domain program.
207    pub domain: PassProgram,
208}
209
210impl PassStages {
211    /// The program of `stage`.
212    pub fn stage(&self, stage: Stage) -> &PassProgram {
213        match stage {
214            Stage::Vertex => &self.vertex,
215            Stage::Fragment => &self.fragment,
216            Stage::Geometry => &self.geometry,
217            Stage::Hull => &self.hull,
218            Stage::Domain => &self.domain,
219        }
220    }
221}
222
223/// A Shader's compiled programs: `passes[subshader][pass]`, and its per-platform blobs.
224#[derive(Clone, Debug, PartialEq)]
225pub struct ShaderPrograms {
226    /// The shader's name (`m_ParsedForm.m_Name`).
227    pub name: String,
228    /// The passes of each subshader.
229    pub passes: Vec<Vec<PassStages>>,
230    /// Unity's `ShaderCompilerPlatform` of each blob (4: D3D11).
231    pub platforms: Vec<i64>,
232    /// Per platform, per segment: offset, compressed and decompressed length.
233    segments: Vec<Vec<(usize, usize, usize)>>,
234    blob: Vec<u8>,
235}
236
237fn items<'a>(v: &'a Value, k: &str) -> Result<&'a [Value]> {
238    match v.get(k) {
239        Some(Value::Array(Array::Values(items))) => Ok(items),
240        Some(Value::Array(a)) if a.is_empty() => Ok(&[]),
241        None => Ok(&[]),
242        _ => Err(invalid(format!("shader program: {k} is not a list"))),
243    }
244}
245
246/// An array of integers, whatever element type the type tree gave it.
247fn ints(v: &Value) -> Result<Vec<i64>> {
248    let bad = || invalid("shader program: an integer array of another type");
249    Ok(match v {
250        Value::Array(a) => match a {
251            Array::I8(x) => x.iter().map(|&n| i64::from(n)).collect(),
252            Array::U8(x) => x.iter().map(|&n| i64::from(n)).collect(),
253            Array::I16(x) => x.iter().map(|&n| i64::from(n)).collect(),
254            Array::U16(x) => x.iter().map(|&n| i64::from(n)).collect(),
255            Array::I32(x) => x.iter().map(|&n| i64::from(n)).collect(),
256            Array::U32(x) => x.iter().map(|&n| i64::from(n)).collect(),
257            Array::I64(x) => x.clone(),
258            Array::U64(x) => x
259                .iter()
260                .map(|&n| i64::try_from(n).map_err(|_| bad()))
261                .collect::<Result<_>>()?,
262            Array::Values(x) => x
263                .iter()
264                .map(|n| n.as_i64().ok_or_else(bad))
265                .collect::<Result<_>>()?,
266            _ => return Err(bad()),
267        },
268        _ => return Err(bad()),
269    })
270}
271
272fn int(v: &Value, k: &str) -> Result<i64> {
273    v.get(k)
274        .and_then(Value::as_i64)
275        .ok_or_else(|| invalid(format!("shader program: no integer {k}")))
276}
277
278fn u32_of(n: i64, what: &str) -> Result<u32> {
279    u32::try_from(n).map_err(|_| invalid(format!("shader program: {what} {n} out of range")))
280}
281
282/// The name an entry's `m_NameIndex` gives in the pass's name table.
283fn name_of(e: &Value, names: &std::collections::BTreeMap<i64, String>) -> Result<String> {
284    let i = int(e, "m_NameIndex")?;
285    names
286        .get(&i)
287        .cloned()
288        .ok_or_else(|| invalid(format!("shader program: name index {i} past the pass's names")))
289}
290
291/// The vector (`vectors`) and matrix (`matrices`) fields of a constant buffer or struct, by offset.
292fn fields(
293    v: &Value,
294    vectors: &str,
295    matrices: &str,
296    names: &std::collections::BTreeMap<i64, String>,
297) -> Result<Vec<Field>> {
298    let mut fields = Vec::new();
299    for (key, matrix) in [(vectors, false), (matrices, true)] {
300        for f in items(v, key)? {
301            fields.push(Field {
302                name: name_of(f, names)?,
303                offset: u32_of(int(f, "m_Index")?, "field offset")?,
304                kind: int(f, "m_Type")?,
305                shape: if matrix {
306                    Shape::Matrix {
307                        rows: u32_of(int(f, "m_RowCount")?, "matrix rows")?,
308                    }
309                } else {
310                    Shape::Vector {
311                        columns: u32_of(int(f, "m_Dim")?, "vector size")?,
312                    }
313                },
314                array_size: u32_of(int(f, "m_ArraySize")?, "array size")?,
315            });
316        }
317    }
318    fields.sort_by_key(|f| f.offset);
319    Ok(fields)
320}
321
322/// The parameters as stored, their names resolved through the pass's name table.
323fn parameters(v: &Value, names: &std::collections::BTreeMap<i64, String>) -> Result<Parameters> {
324    let name = |e: &Value| name_of(e, names);
325    let bindings = items(v, "m_ConstantBufferBindings")?
326        .iter()
327        .map(|b| {
328            Ok((
329                int(b, "m_NameIndex")?,
330                u32_of(int(b, "m_Index")?, "binding slot")?,
331            ))
332        })
333        .collect::<Result<std::collections::BTreeMap<i64, u32>>>()?;
334    let mut constant_buffers = Vec::new();
335    for cb in items(v, "m_ConstantBuffers")? {
336        let mut structs = items(cb, "m_StructParams")?
337            .iter()
338            .map(|s| {
339                Ok(StructField {
340                    name: name(s)?,
341                    offset: u32_of(int(s, "m_Index")?, "struct offset")?,
342                    array_size: u32_of(int(s, "m_ArraySize")?, "array size")?,
343                    size: u32_of(int(s, "m_StructSize")?, "struct size")?,
344                    members: fields(s, "m_VectorMembers", "m_MatrixMembers", names)?,
345                })
346            })
347            .collect::<Result<Vec<_>>>()?;
348        structs.sort_by_key(|s| s.offset);
349        constant_buffers.push(ConstantBuffer {
350            name: name(cb)?,
351            slot: bindings.get(&int(cb, "m_NameIndex")?).copied(),
352            size: u32_of(int(cb, "m_Size")?, "buffer size")?,
353            fields: fields(cb, "m_VectorParams", "m_MatrixParams", names)?,
354            structs,
355        });
356    }
357    let textures = items(v, "m_TextureParams")?
358        .iter()
359        .map(|t| {
360            let sampler = int(t, "m_SamplerIndex")?;
361            Ok(TextureParam {
362                name: name(t)?,
363                slot: u32_of(int(t, "m_Index")?, "texture slot")?,
364                sampler: (sampler >= 0)
365                    .then(|| u32_of(sampler, "sampler slot"))
366                    .transpose()?,
367                dim: int(t, "m_Dim")?,
368            })
369        })
370        .collect::<Result<_>>()?;
371    let samplers = items(v, "m_Samplers")?
372        .iter()
373        .map(|s| {
374            Ok((
375                u32_of(int(s, "sampler")?, "sampler state")?,
376                u32_of(int(s, "bindPoint")?, "sampler slot")?,
377            ))
378        })
379        .collect::<Result<_>>()?;
380    Ok(Parameters {
381        constant_buffers,
382        textures,
383        samplers,
384    })
385}
386
387/// The first word of a parameter blob entry in a 2021.2 player.
388const PARAMETER_BLOB_MAGIC: u32 = 0x0C0A_75BA;
389
390/// A length-prefixed string of a parameter blob: its length, its bytes, padding to four.
391fn blob_string(r: &mut crate::reader::Reader<'_>) -> Result<String> {
392    let n = usize::try_from(r.u32()?).map_err(|_| invalid("parameter blob: a string length"))?;
393    let bytes = r.take(n)?;
394    r.align(4)?;
395    String::from_utf8(bytes.to_vec()).map_err(|_| invalid("parameter blob: a name that is not UTF-8"))
396}
397
398/// A field of a constant buffer or struct: its name, then type, rows, columns, whether it is a matrix, array size and
399/// byte offset (each a word).
400fn blob_field(r: &mut crate::reader::Reader<'_>) -> Result<Field> {
401    let name = blob_string(r)?;
402    let kind = i64::from(r.u32()?);
403    let (rows, columns, matrix) = (r.u32()?, r.u32()?, r.u32()?);
404    let array_size = r.u32()?;
405    let offset = r.u32()?;
406    let shape = match matrix {
407        0 => Shape::Vector { columns },
408        1 => Shape::Matrix { rows },
409        n => {
410            return Err(invalid(format!(
411                "parameter blob: field {name} has matrix flag {n}"
412            )));
413        }
414    };
415    Ok(Field {
416        name,
417        offset,
418        kind,
419        shape,
420        array_size,
421    })
422}
423
424/// A variant's own parameters as a 2021.2 player stores them in the blob entry `m_ParameterBlobIndices` names:
425/// the magic word, a count of the constant buffers plus one, four zero words, then each constant buffer (name, size,
426/// its fields, its structs: name, offset, array size, size and fields) and a list of entries, each a name, a kind and
427/// a slot: kind 0 a texture (the sampler slot, `u32::MAX` for none, and a dimension word), kind 1 a constant buffer's
428/// binding (a word, 0), kind 4 a sampler (its packed state; the name is empty). The layout was held to every one of
429/// the 3,280 entries of Sledding Game's `level1` shaders (3,272 read to the last byte; the 8 refused hold a kind 2,
430/// a structured buffer of the DOTS instancing, whose payload is not read).
431pub fn parameter_blob(entry: &[u8]) -> Result<Parameters> {
432    let mut r = crate::reader::Reader::new(entry, false);
433    if r.u32()? != PARAMETER_BLOB_MAGIC {
434        return Err(invalid("parameter blob: not the 2021.2 format"));
435    }
436    let buffers = r.u32()?;
437    if (0..4).any(|_| r.u32().map_or(true, |w| w != 0)) {
438        return Err(invalid(
439            "parameter blob: words before the buffers that are not zero",
440        ));
441    }
442    let mut constant_buffers = Vec::new();
443    // the buffers are the entries up to the list's count word: a buffer's name is a string, the count is not one
444    for _ in 1..buffers {
445        let name = blob_string(&mut r)?;
446        let size = r.u32()?;
447        let fields = (0..r.u32()?)
448            .map(|_| blob_field(&mut r))
449            .collect::<Result<Vec<_>>>()?;
450        let mut structs = Vec::new();
451        for _ in 0..r.u32()? {
452            structs.push(StructField {
453                name: blob_string(&mut r)?,
454                offset: r.u32()?,
455                array_size: r.u32()?,
456                size: r.u32()?,
457                members: (0..r.u32()?)
458                    .map(|_| blob_field(&mut r))
459                    .collect::<Result<Vec<_>>>()?,
460            });
461        }
462        constant_buffers.push(ConstantBuffer {
463            name,
464            slot: None,
465            size,
466            fields,
467            structs,
468        });
469    }
470    let (mut textures, mut samplers) = (Vec::new(), Vec::new());
471    for _ in 0..r.u32()? {
472        let name = blob_string(&mut r)?;
473        let (kind, slot) = (r.u32()?, r.u32()?);
474        match kind {
475            0 => {
476                let sampler = r.u32()?;
477                textures.push(TextureParam {
478                    name,
479                    slot,
480                    sampler: (sampler != u32::MAX).then_some(sampler),
481                    dim: i64::from(r.u32()?),
482                });
483            }
484            1 => {
485                r.u32()?;
486                // a buffer the blob binds and holds none of is the pass's common one: an empty buffer carries the slot
487                match constant_buffers.iter_mut().find(|cb| cb.name == name) {
488                    Some(cb) => cb.slot = Some(slot),
489                    None => constant_buffers.push(ConstantBuffer {
490                        name,
491                        slot: Some(slot),
492                        size: 0,
493                        fields: Vec::new(),
494                        structs: Vec::new(),
495                    }),
496                }
497            }
498            4 => samplers.push((r.u32()?, slot)),
499            n => return Err(invalid(format!("parameter blob: an entry of kind {n}"))),
500        }
501    }
502    if r.remaining() != 0 {
503        return Err(invalid("parameter blob: bytes after the entries"));
504    }
505    Ok(Parameters {
506        constant_buffers,
507        textures,
508        samplers,
509    })
510}
511
512/// A variant's parameters from the pass's common ones and its own blob: the buffers by name (fields and structs of
513/// both, the blob's taking precedence; the larger size; the blob's slot, else the common one), then the textures and
514/// samplers of both.
515pub fn merge_parameters(common: &Parameters, own: &Parameters) -> Parameters {
516    let mut buffers = own.constant_buffers.clone();
517    for c in &common.constant_buffers {
518        match buffers.iter_mut().find(|b| b.name == c.name) {
519            Some(b) => {
520                for f in &c.fields {
521                    if !b.fields.iter().any(|g| g.name == f.name && g.offset == f.offset) {
522                        b.fields.push(f.clone());
523                    }
524                }
525                b.fields.sort_by_key(|f| f.offset);
526                for s in &c.structs {
527                    if !b.structs.iter().any(|g| g.name == s.name) {
528                        b.structs.push(s.clone());
529                    }
530                }
531                b.structs.sort_by_key(|s| s.offset);
532                b.size = b.size.max(c.size);
533                b.slot = b.slot.or(c.slot);
534            }
535            None => buffers.push(c.clone()),
536        }
537    }
538    let mut textures = own.textures.clone();
539    textures.extend(
540        common
541            .textures
542            .iter()
543            .filter(|t| !own.textures.iter().any(|o| o.slot == t.slot))
544            .cloned(),
545    );
546    let mut samplers = own.samplers.clone();
547    samplers.extend(
548        common
549            .samplers
550            .iter()
551            .filter(|s| !own.samplers.contains(s))
552            .copied(),
553    );
554    Parameters {
555        constant_buffers: buffers,
556        textures,
557        samplers,
558    }
559}
560
561/// One stage's program of a pass as stored (`program` is its `prog*` value): its variants and common parameters, names
562/// resolved through the pass's name table `names` and the form's `keyword_names`; `shader` names the shader in errors.
563fn stage_program(
564    program: &Value,
565    names: &std::collections::BTreeMap<i64, String>,
566    keyword_names: &[String],
567    name: &str,
568) -> Result<PassProgram> {
569    let mut variants = Vec::new();
570    // before 2021.2: one list, each variant with its tier, keywords by name index and own parameters
571    for v in items(program, "m_SubPrograms")? {
572        let mut keywords = Vec::new();
573        for k in ["m_GlobalKeywordIndices", "m_LocalKeywordIndices"] {
574            if let Some(k) = v.get(k) {
575                for i in ints(k)? {
576                    keywords.push(names.get(&i).cloned().ok_or_else(|| {
577                        invalid(format!("shader {name}: keyword index {i} past the pass's names"))
578                    })?);
579                }
580            }
581        }
582        variants.push(Variant {
583            group: usize::try_from(int(v, "m_ShaderHardwareTier")?)
584                .map_err(|_| invalid("shader program: a negative hardware tier"))?,
585            blob_index: u32_of(int(v, "m_BlobIndex")?, "blob index")?,
586            keywords,
587            gpu_program_type: int(v, "m_GpuProgramType")?,
588            requirements: v.get("m_ShaderRequirements").and_then(Value::as_i64).unwrap_or(0),
589            parameter_blob: None,
590            parameters: Some(parameters(v, names)?),
591        });
592    }
593    // the blob entry of each variant's own parameters, by group and place in the group's list
594    let parameter_blobs: Vec<Vec<i64>> = match program.get("m_ParameterBlobIndices") {
595        Some(Value::Array(Array::Values(groups))) => groups.iter().map(ints).collect::<Result<_>>()?,
596        _ => Vec::new(),
597    };
598    for (group, list) in items(program, "m_PlayerSubPrograms")?.iter().enumerate() {
599        let list: &[Value] = match list {
600            Value::Array(Array::Values(x)) => x,
601            Value::Array(a) if a.is_empty() => &[],
602            _ => return Err(invalid("shader pass: a player sub-program list")),
603        };
604        for (place, v) in list.iter().enumerate() {
605            let parameter_blob = parameter_blobs
606                .get(group)
607                .and_then(|g| g.get(place))
608                .map(|i| u32_of(*i, "parameter blob index"))
609                .transpose()?;
610            let keywords = match v.get("m_KeywordIndices") {
611                Some(k) => ints(k)?
612                    .into_iter()
613                    .map(|i| {
614                        usize::try_from(i)
615                            .ok()
616                            .and_then(|i| keyword_names.get(i))
617                            .cloned()
618                            .ok_or_else(|| invalid(format!("shader {name}: keyword index {i}")))
619                    })
620                    .collect::<Result<_>>()?,
621                None => Vec::new(),
622            };
623            variants.push(Variant {
624                group,
625                blob_index: u32_of(int(v, "m_BlobIndex")?, "blob index")?,
626                keywords,
627                gpu_program_type: int(v, "m_GpuProgramType")?,
628                requirements: v.get("m_ShaderRequirements").and_then(Value::as_i64).unwrap_or(0),
629                parameter_blob,
630                parameters: None,
631            });
632        }
633    }
634    let parameters = match program.get("m_CommonParameters") {
635        Some(c) => parameters(c, names)?,
636        None => Parameters::default(),
637    };
638    Ok(PassProgram { variants, parameters })
639}
640
641/// Blob segments of a [`ShaderPrograms`] already decompressed ([`ShaderPrograms::code_with`]).
642#[derive(Debug, Default)]
643pub struct Segments(std::collections::BTreeMap<(usize, usize), std::sync::Arc<Vec<u8>>>);
644
645impl Segments {
646    fn get(
647        &mut self,
648        programs: &ShaderPrograms,
649        platform: usize,
650        k: usize,
651    ) -> Result<std::sync::Arc<Vec<u8>>> {
652        if let Some(s) = self.0.get(&(platform, k)) {
653            return Ok(s.clone());
654        }
655        let s = std::sync::Arc::new(programs.segment(platform, k)?);
656        self.0.insert((platform, k), s.clone());
657        Ok(s)
658    }
659}
660
661impl ShaderPrograms {
662    /// Read the Shader `r` of `player`.
663    pub fn read(player: &Player, r: &ObjRef) -> Result<Self> {
664        let s = player.decode(r)?;
665        let form = s
666            .get("m_ParsedForm")
667            .ok_or_else(|| invalid(format!("shader {}/{}: no m_ParsedForm", r.file, r.path_id)))?;
668        let name = form
669            .get("m_Name")
670            .and_then(Value::as_str)
671            .unwrap_or("")
672            .to_owned();
673        let keyword_names: Vec<String> = items(form, "m_KeywordNames")?
674            .iter()
675            .map(|k| {
676                k.as_str()
677                    .map(str::to_owned)
678                    .ok_or_else(|| invalid("shader: a keyword name"))
679            })
680            .collect::<Result<_>>()?;
681        let mut passes = Vec::new();
682        for sub in items(form, "m_SubShaders")? {
683            let mut out = Vec::new();
684            for p in items(sub, "m_Passes")? {
685                // the pass's name table: name -> index
686                let mut names = std::collections::BTreeMap::new();
687                for pair in items(p, "m_NameIndices")? {
688                    let Value::Pair(kv) = pair else {
689                        return Err(invalid("shader pass: m_NameIndices entry is not a pair"));
690                    };
691                    let key = kv.0.as_str().ok_or_else(|| invalid("shader pass: a name"))?;
692                    let index =
693                        kv.1.as_i64()
694                            .ok_or_else(|| invalid("shader pass: a name index"))?;
695                    names.insert(index, key.to_owned());
696                }
697                let stage = |stage: Stage| match p.get(stage.field()) {
698                    Some(program) => stage_program(program, &names, &keyword_names, &name),
699                    None => Ok(PassProgram::default()),
700                };
701                out.push(PassStages {
702                    vertex: stage(Stage::Vertex)?,
703                    fragment: stage(Stage::Fragment)?,
704                    geometry: stage(Stage::Geometry)?,
705                    hull: stage(Stage::Hull)?,
706                    domain: stage(Stage::Domain)?,
707                });
708            }
709            passes.push(out);
710        }
711        let platforms = s.get("platforms").map(ints).transpose()?.unwrap_or_default();
712        let per = |k: &str| -> Result<Vec<Vec<i64>>> {
713            items(&s, k)?
714                .iter()
715                .map(|v| match v {
716                    Value::Array(_) => ints(v),
717                    x => Ok(vec![
718                        x.as_i64().ok_or_else(|| invalid(format!("shader: {k} entry")))?,
719                    ]),
720                })
721                .collect()
722        };
723        let (offsets, packed, unpacked) = (
724            per("offsets")?,
725            per("compressedLengths")?,
726            per("decompressedLengths")?,
727        );
728        if offsets.len() != platforms.len()
729            || packed.len() != platforms.len()
730            || unpacked.len() != platforms.len()
731        {
732            return Err(invalid(format!(
733                "shader {name}: blob tables of another length than its platforms"
734            )));
735        }
736        let segments = offsets
737            .iter()
738            .zip(&packed)
739            .zip(&unpacked)
740            .map(|((o, p), u)| {
741                if o.len() != p.len() || o.len() != u.len() {
742                    return Err(invalid(format!(
743                        "shader {name}: segment tables of different lengths"
744                    )));
745                }
746                let size = |n: i64| usize::try_from(n).map_err(|_| invalid("shader: a negative blob size"));
747                o.iter()
748                    .zip(p)
749                    .zip(u)
750                    .map(|((&o, &p), &u)| Ok((size(o)?, size(p)?, size(u)?)))
751                    .collect()
752            })
753            .collect::<Result<_>>()?;
754        let blob = s
755            .get("compressedBlob")
756            .and_then(Value::as_bytes)
757            .map(<[u8]>::to_vec)
758            .unwrap_or_default();
759        Ok(ShaderPrograms {
760            name,
761            passes,
762            platforms,
763            segments,
764            blob,
765        })
766    }
767
768    /// Segment `k` of platform `platform`'s blob, decompressed.
769    fn segment(&self, platform: usize, k: usize) -> Result<Vec<u8>> {
770        let &(offset, packed, unpacked) = self
771            .segments
772            .get(platform)
773            .and_then(|s| s.get(k))
774            .ok_or_else(|| invalid(format!("shader {}: no blob segment {k}", self.name)))?;
775        let bytes = offset
776            .checked_add(packed)
777            .and_then(|end| self.blob.get(offset..end))
778            .ok_or_else(|| invalid(format!("shader {}: blob segment {k} past the blob", self.name)))?;
779        Lz4.decompress(bytes, unpacked)
780            .map_err(|e| invalid(format!("shader {}: blob segment {k}: {e}", self.name)))
781    }
782
783    /// The code of blob entry `index` of the blob for `platform` (Unity's `ShaderCompilerPlatform`): the program
784    /// record's code bytes, as stored.
785    pub fn code(&self, platform: i64, index: u32) -> Result<Vec<u8>> {
786        self.code_with(&mut Segments::default(), platform, index)
787    }
788
789    /// [`ShaderPrograms::code`] keeping the segments it decompresses in `cache`, which reads many entries of one
790    /// shader without decompressing a segment for each.
791    pub fn code_with(&self, cache: &mut Segments, platform: i64, index: u32) -> Result<Vec<u8>> {
792        let entry = self.entry_with(cache, platform, index)?;
793        Self::code_of(&entry)
794    }
795
796    /// The bytes of blob entry `index` of the blob for `platform`, as stored.
797    pub fn entry_with(&self, cache: &mut Segments, platform: i64, index: u32) -> Result<Vec<u8>> {
798        let p = self
799            .platforms
800            .iter()
801            .position(|&x| x == platform)
802            .ok_or_else(|| invalid(format!("shader {}: no blob for platform {platform}", self.name)))?;
803        let table = cache.get(self, p, 0)?;
804        let mut r = crate::reader::Reader::new(&table, false);
805        let count = usize::try_from(r.i32()?).map_err(|_| invalid("shader blob: a negative entry count"))?;
806        if index as usize >= count {
807            return Err(invalid(format!(
808                "shader {}: blob entry {index} of {count}",
809                self.name
810            )));
811        }
812        // the table's count is its own word: the entry has to be in the bytes there are
813        let at = usize::try_from(index)
814            .ok()
815            .and_then(|i| i.checked_mul(12))
816            .and_then(|o| o.checked_add(4))
817            .filter(|at| at.saturating_add(12) <= table.len())
818            .ok_or_else(|| {
819                invalid(format!(
820                    "shader {}: blob entry {index} of {count} in a table of {} bytes",
821                    self.name,
822                    table.len()
823                ))
824            })?;
825        r.pos = at;
826        let (offset, length, segment) = (r.i32()?, r.i32()?, r.i32()?);
827        let span = |n: i32| usize::try_from(n).map_err(|_| invalid("shader blob: a negative entry span"));
828        let (offset, length, segment) = (span(offset)?, span(length)?, span(segment)?);
829        let data = if segment == 0 {
830            table
831        } else {
832            cache.get(self, p, segment)?
833        };
834        let entry = offset
835            .checked_add(length)
836            .and_then(|end| data.get(offset..end))
837            .ok_or_else(|| {
838                invalid(format!(
839                    "shader {}: blob entry {index} past its segment",
840                    self.name
841                ))
842            })?;
843        Ok(entry.to_vec())
844    }
845
846    /// The code of a program record (`entry`): the record's version, program type, a 12-byte header, (2016.2+) 4
847    /// more, keywords, code.
848    fn code_of(entry: &[u8]) -> Result<Vec<u8>> {
849        let mut r = crate::reader::Reader::new(entry, false);
850        let version = r.i32()?;
851        let _program_type = r.i32()?;
852        r.take(12)?;
853        if version >= 201_608_170 {
854            r.take(4)?;
855        }
856        let strings = |r: &mut crate::reader::Reader<'_>| -> Result<()> {
857            let n =
858                usize::try_from(r.i32()?).map_err(|_| invalid("shader blob: a negative keyword count"))?;
859            for _ in 0..n {
860                let len = usize::try_from(r.i32()?)
861                    .map_err(|_| invalid("shader blob: a negative string length"))?;
862                r.take(len)?;
863                r.align(4)?;
864            }
865            Ok(())
866        };
867        strings(&mut r)?;
868        if (201_806_140..202_012_090).contains(&version) {
869            strings(&mut r)?;
870        }
871        let len = usize::try_from(r.i32()?).map_err(|_| invalid("shader blob: a negative code length"))?;
872        Ok(r.take(len)?.to_vec())
873    }
874}
875
876/// Unity's `ShaderGpuProgramType` of the D3D11 programs: vertex SM 4.0 and 5.0 (15, 16), pixel (17, 18), geometry (19,
877/// 20), hull (21), domain (22).
878pub const D3D11_PROGRAM_TYPES: std::ops::RangeInclusive<i64> = 15..=22;
879
880/// The DXBC container in a D3D11 program record's code: Unity stores a short header before it. `None` when the code
881/// holds no container (the first `DXBC` within its first 64 bytes, whole as its own header sizes it).
882pub fn dxbc(code: &[u8]) -> Option<&[u8]> {
883    (0..code.len().min(64)).find_map(|at| {
884        let c = code.get(at..)?;
885        if c.get(..4)? != b"DXBC" {
886            return None;
887        }
888        let size = u32::from_le_bytes(c.get(24..28)?.try_into().ok()?) as usize;
889        c.get(..size)
890    })
891}
892
893/// The properties a decoded shader declares (`m_ParsedForm.m_PropInfo.m_Props`); none where it states none.
894fn properties(s: &Value) -> Result<&[Value]> {
895    Ok(s.get("m_ParsedForm")
896        .and_then(|f| f.get("m_PropInfo"))
897        .map(|p| items(p, "m_Props"))
898        .transpose()?
899        .unwrap_or(&[]))
900}
901
902/// The default each texture property of shader `r` declares (`m_ParsedForm.m_PropInfo.m_Props`, type 4:
903/// `m_DefTexture.m_DefaultName`), by property.
904pub fn texture_defaults(player: &Player, r: &ObjRef) -> Result<Vec<(String, String)>> {
905    let s = player.decode(r)?;
906    let props = properties(&s)?;
907    let mut out = Vec::new();
908    for p in props {
909        if p.get("m_Type").and_then(Value::as_i64) != Some(4) {
910            continue;
911        }
912        let name = p.get("m_Name").and_then(Value::as_str);
913        let default = p
914            .get("m_DefTexture")
915            .and_then(|d| d.get("m_DefaultName"))
916            .and_then(Value::as_str);
917        if let (Some(name), Some(default)) = (name, default) {
918            out.push((name.to_owned(), default.to_owned()));
919        }
920    }
921    Ok(out)
922}
923
924/// Unity's `ShaderPropertyFlags.HDR`: a colour property declared `[HDR]`.
925pub const PROPERTY_HDR: u32 = 16;
926
927/// A colour property a shader declares (`m_ParsedForm.m_PropInfo.m_Props`, type 0) and its `m_Flags` (Unity's
928/// `ShaderPropertyFlags`).
929#[derive(Clone, Debug, PartialEq, Eq)]
930pub struct ColourProperty {
931    /// The property's name (`_BaseColor`).
932    pub name: String,
933    /// Its `m_Flags` (`ShaderPropertyFlags`; [`PROPERTY_HDR`] is `[HDR]`).
934    pub flags: u32,
935}
936
937impl ColourProperty {
938    /// Whether the property is declared `[HDR]` ([`PROPERTY_HDR`]). In a linear colour space player Unity hands an HDR
939    /// colour's stored value to the programs as it is, and converts any other colour from gamma to linear.
940    pub fn hdr(&self) -> bool {
941        self.flags & PROPERTY_HDR != 0
942    }
943}
944
945/// The colour properties shader `r` declares, with their flags, in its order.
946pub fn colour_properties(player: &Player, r: &ObjRef) -> Result<Vec<ColourProperty>> {
947    let s = player.decode(r)?;
948    let mut out = Vec::new();
949    for p in properties(&s)? {
950        if p.get("m_Type").and_then(Value::as_i64) != Some(0) {
951            continue;
952        }
953        let name = p
954            .get("m_Name")
955            .and_then(Value::as_str)
956            .ok_or_else(|| invalid("shader property: a colour property has no m_Name"))?;
957        let flags = p
958            .get("m_Flags")
959            .and_then(Value::as_i64)
960            .and_then(|f| u32::try_from(f).ok())
961            .ok_or_else(|| invalid(format!("shader property {name}: m_Flags is not a u32")))?;
962        out.push(ColourProperty {
963            name: name.to_owned(),
964            flags,
965        });
966    }
967    Ok(out)
968}
969
970#[cfg(test)]
971mod tests {
972    use super::*;
973
974    fn word(out: &mut Vec<u8>, w: u32) {
975        out.extend_from_slice(&w.to_le_bytes());
976    }
977
978    fn text(out: &mut Vec<u8>, s: &str) {
979        word(out, s.len() as u32);
980        out.extend_from_slice(s.as_bytes());
981        while !out.len().is_multiple_of(4) {
982            out.push(0);
983        }
984    }
985
986    /// A blob entry as a 2021.2 player lays it out: two buffers (`$Globals` with a vector and a matrix, and
987    /// `UnityPerMaterial` with none), two textures, the binding of `UnityPerMaterial` to slot 6, and a sampler.
988    fn blob() -> Vec<u8> {
989        let mut b = Vec::new();
990        for w in [PARAMETER_BLOB_MAGIC, 3, 0, 0, 0, 0] {
991            word(&mut b, w);
992        }
993        text(&mut b, "$Globals");
994        word(&mut b, 128);
995        word(&mut b, 2);
996        text(&mut b, "_Color");
997        for w in [0, 1, 4, 0, 0, 16] {
998            word(&mut b, w);
999        }
1000        text(&mut b, "unity_MatrixV");
1001        for w in [0, 4, 4, 1, 0, 64] {
1002            word(&mut b, w);
1003        }
1004        word(&mut b, 0);
1005        text(&mut b, "UnityPerMaterial");
1006        for w in [208, 0, 0] {
1007            word(&mut b, w);
1008        }
1009        word(&mut b, 4);
1010        text(&mut b, "_IceTex");
1011        for w in [0, 0, 4, 4] {
1012            word(&mut b, w);
1013        }
1014        text(&mut b, "_Roughness");
1015        for w in [0, 7, u32::MAX, 4] {
1016            word(&mut b, w);
1017        }
1018        text(&mut b, "UnityPerMaterial");
1019        for w in [1, 6, 0] {
1020            word(&mut b, w);
1021        }
1022        text(&mut b, "");
1023        for w in [4, 3, 341] {
1024            word(&mut b, w);
1025        }
1026        b
1027    }
1028
1029    fn four_wide(name: &str, offset: u32) -> Field {
1030        Field {
1031            name: name.into(),
1032            offset,
1033            kind: 0,
1034            shape: Shape::Vector { columns: 4 },
1035            array_size: 0,
1036        }
1037    }
1038
1039    #[test]
1040    fn a_parameter_blob_gives_buffers_slots_textures_and_samplers() {
1041        let p = parameter_blob(&blob()).unwrap();
1042        assert_eq!(p.constant_buffers.len(), 2);
1043        let globals = &p.constant_buffers[0];
1044        assert_eq!(
1045            (globals.name.as_str(), globals.size, globals.slot),
1046            ("$Globals", 128, None)
1047        );
1048        assert_eq!(
1049            globals.fields,
1050            [
1051                four_wide("_Color", 16),
1052                Field {
1053                    name: "unity_MatrixV".into(),
1054                    offset: 64,
1055                    kind: 0,
1056                    shape: Shape::Matrix { rows: 4 },
1057                    array_size: 0
1058                },
1059            ]
1060        );
1061        assert_eq!(p.constant_buffers[1].slot, Some(6));
1062        assert_eq!(
1063            p.textures,
1064            [
1065                TextureParam {
1066                    name: "_IceTex".into(),
1067                    slot: 0,
1068                    sampler: Some(4),
1069                    dim: 4
1070                },
1071                TextureParam {
1072                    name: "_Roughness".into(),
1073                    slot: 7,
1074                    sampler: None,
1075                    dim: 4
1076                },
1077            ]
1078        );
1079        assert_eq!(p.samplers, [(341, 3)]);
1080    }
1081
1082    #[test]
1083    fn a_parameter_blob_that_is_not_the_format_or_holds_what_is_not_read_is_refused() {
1084        let mut other = blob();
1085        other[0] ^= 1;
1086        assert!(parameter_blob(&other).is_err());
1087        // bytes after the last entry
1088        let mut long = blob();
1089        long.extend_from_slice(&[0; 4]);
1090        assert!(parameter_blob(&long).is_err());
1091        // a kind 2 entry (a structured buffer) is not read: the sampler entry's last three words become kind 2
1092        let mut structured = blob();
1093        let at = structured.len() - 12;
1094        structured[at..at + 4].copy_from_slice(&2u32.to_le_bytes());
1095        assert!(parameter_blob(&structured).is_err());
1096        // cut short
1097        assert!(parameter_blob(&blob()[..40]).is_err());
1098    }
1099
1100    #[test]
1101    fn a_variants_parameters_are_its_blobs_with_the_pass_common_ones_added() {
1102        let own = parameter_blob(&blob()).unwrap();
1103        let common = Parameters {
1104            constant_buffers: vec![
1105                ConstantBuffer {
1106                    name: "$Globals".into(),
1107                    slot: Some(0),
1108                    size: 256,
1109                    fields: vec![four_wide("_Color", 16), four_wide("_Other", 32)],
1110                    structs: Vec::new(),
1111                },
1112                ConstantBuffer {
1113                    name: "AdditionalLights".into(),
1114                    slot: Some(1),
1115                    size: 24_576,
1116                    fields: vec![four_wide("_Lights", 0)],
1117                    structs: Vec::new(),
1118                },
1119            ],
1120            textures: vec![TextureParam {
1121                name: "_Common".into(),
1122                slot: 0,
1123                sampler: None,
1124                dim: 4,
1125            }],
1126            samplers: vec![(1, 0), (341, 3)],
1127        };
1128        let merged = merge_parameters(&common, &own);
1129        let names: Vec<&str> = merged.constant_buffers.iter().map(|c| c.name.as_str()).collect();
1130        assert_eq!(names, ["$Globals", "UnityPerMaterial", "AdditionalLights"]);
1131        let globals = &merged.constant_buffers[0];
1132        // the blob's fields and the common one it lacks, in offset order; the larger size; the common slot
1133        let fields: Vec<(&str, u32)> = globals
1134            .fields
1135            .iter()
1136            .map(|f| (f.name.as_str(), f.offset))
1137            .collect();
1138        assert_eq!(fields, [("_Color", 16), ("_Other", 32), ("unity_MatrixV", 64)]);
1139        assert_eq!((globals.size, globals.slot), (256, Some(0)));
1140        assert_eq!(merged.constant_buffers[1].slot, Some(6));
1141        // a common texture in the slot the blob names is the blob's: it is not kept twice
1142        assert_eq!(merged.textures.len(), 2);
1143        assert_eq!(merged.textures[0].name, "_IceTex");
1144        assert_eq!(merged.samplers, [(341, 3), (1, 0)]);
1145    }
1146
1147    /// A blob table that states more entries than it holds: the entry is refused, not indexed past the table.
1148    #[test]
1149    fn a_blob_entry_past_the_table_is_refused() {
1150        // one LZ4 literal run of the four bytes of a count of 1000
1151        let table = vec![0x40u8, 0xE8, 0x03, 0, 0];
1152        let programs = ShaderPrograms {
1153            name: "s".into(),
1154            passes: vec![],
1155            platforms: vec![4],
1156            segments: vec![vec![(0, table.len(), 4)]],
1157            blob: table,
1158        };
1159        let e = programs.code(4, 999).unwrap_err().to_string();
1160        assert!(e.contains("blob entry 999 of 1000 in a table of 4 bytes"), "{e}");
1161    }
1162    use std::sync::Arc;
1163
1164    #[test]
1165    fn the_dxbc_container_is_found_past_unitys_header() {
1166        let mut container = b"DXBC".to_vec();
1167        container.extend([0; 20]);
1168        container.extend(32u32.to_le_bytes());
1169        container.extend([0; 4]);
1170        let mut code = vec![7, 0, 0, 0, 1, 2];
1171        code.extend(&container);
1172        code.extend([9, 9]);
1173        assert_eq!(dxbc(&code), Some(&container[..]));
1174        assert_eq!(dxbc(&code[..20]), None);
1175    }
1176
1177    fn obj(fields: Vec<(&str, Value)>) -> Value {
1178        Value::Struct(fields.into_iter().map(|(k, v)| (Arc::from(k), v)).collect())
1179    }
1180
1181    fn list(items: Vec<Value>) -> Value {
1182        Value::Array(Array::Values(items))
1183    }
1184
1185    fn vector(name: i64, offset: i64, dim: i64) -> Value {
1186        obj(vec![
1187            ("m_NameIndex", Value::Int(name)),
1188            ("m_Index", Value::Int(offset)),
1189            ("m_ArraySize", Value::Int(0)),
1190            ("m_Type", Value::Int(0)),
1191            ("m_Dim", Value::Int(dim)),
1192        ])
1193    }
1194
1195    fn cbuffer(fields: Vec<Value>, structs: Vec<Value>) -> Value {
1196        obj(vec![
1197            ("m_NameIndex", Value::Int(0)),
1198            ("m_VectorParams", list(fields)),
1199            ("m_MatrixParams", list(Vec::new())),
1200            ("m_StructParams", list(structs)),
1201            ("m_Size", Value::Int(32)),
1202        ])
1203    }
1204
1205    fn params(cbs: Vec<Value>) -> Value {
1206        obj(vec![
1207            ("m_ConstantBuffers", list(cbs)),
1208            (
1209                "m_ConstantBufferBindings",
1210                list(vec![obj(vec![
1211                    ("m_NameIndex", Value::Int(0)),
1212                    ("m_Index", Value::Int(3)),
1213                ])]),
1214            ),
1215        ])
1216    }
1217
1218    #[test]
1219    fn parameters_resolve_names_slots_offsets_and_structs_and_refuse_what_they_cannot() {
1220        let names: std::collections::BTreeMap<i64, String> = [
1221            (0, "UnityPerMaterial".to_owned()),
1222            (1, "_BaseColor".to_owned()),
1223            (2, "_Smoothness".to_owned()),
1224        ]
1225        .into_iter()
1226        .collect();
1227        // fields come out by offset, the buffer at the slot its binding names
1228        let p = parameters(
1229            &params(vec![cbuffer(vec![vector(2, 16, 1), vector(1, 0, 4)], Vec::new())]),
1230            &names,
1231        )
1232        .unwrap();
1233        let cb = &p.constant_buffers[0];
1234        assert_eq!(
1235            (cb.name.as_str(), cb.slot, cb.size),
1236            ("UnityPerMaterial", Some(3), 32)
1237        );
1238        assert_eq!(
1239            cb.fields
1240                .iter()
1241                .map(|f| (f.name.as_str(), f.offset, f.shape))
1242                .collect::<Vec<_>>(),
1243            vec![
1244                ("_BaseColor", 0, Shape::Vector { columns: 4 }),
1245                ("_Smoothness", 16, Shape::Vector { columns: 1 })
1246            ]
1247        );
1248        // a struct field: an array of members (instancing's per-draw arrays)
1249        let instanced = obj(vec![
1250            ("m_NameIndex", Value::Int(1)),
1251            ("m_Index", Value::Int(0)),
1252            ("m_ArraySize", Value::Int(2)),
1253            ("m_StructSize", Value::Int(32)),
1254            ("m_VectorMembers", list(vec![vector(2, 16, 4)])),
1255            ("m_MatrixMembers", list(Vec::new())),
1256        ]);
1257        let p = parameters(&params(vec![cbuffer(Vec::new(), vec![instanced])]), &names).unwrap();
1258        let s = &p.constant_buffers[0].structs[0];
1259        assert_eq!(
1260            (s.name.as_str(), s.offset, s.array_size, s.size),
1261            ("_BaseColor", 0, 2, 32)
1262        );
1263        assert_eq!(
1264            s.members
1265                .iter()
1266                .map(|f| (f.name.as_str(), f.offset))
1267                .collect::<Vec<_>>(),
1268            vec![("_Smoothness", 16)]
1269        );
1270        // a name index past the table and a negative offset are refused
1271        assert!(parameters(&params(vec![cbuffer(vec![vector(9, 0, 4)], Vec::new())]), &names).is_err());
1272        assert!(parameters(&params(vec![cbuffer(vec![vector(1, -4, 4)], Vec::new())]), &names).is_err());
1273    }
1274
1275    #[test]
1276    fn integer_arrays_read_whatever_their_element_type() {
1277        assert_eq!(ints(&Value::Array(Array::U16(vec![1, 2]))).unwrap(), vec![1, 2]);
1278        assert_eq!(
1279            ints(&list(vec![Value::Int(-1), Value::UInt(5)])).unwrap(),
1280            vec![-1, 5]
1281        );
1282        assert!(ints(&Value::Array(Array::F32(vec![1.0]))).is_err());
1283        assert!(ints(&Value::Int(1)).is_err());
1284    }
1285}