iridia-interface 0.1.0

A shader program's interface as plain serializable data: varyings, resources, constant layouts, fixed-function state and what its code shows
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
//! A program's resources: constant buffers with explicit layouts, textures, samplers, buffers and storage.

use serde::Serialize;

use super::{ScalarType, Value};

/// Names a resource within one [`super::Interface`]; the reader numbers them in its source's order.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
pub struct ResourceId(pub u32);

/// The dimensionality of a texture or image. Whether it is an array or multisampled is separate.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum TextureDim {
    /// A one-dimensional texture or image.
    D1,
    /// A two-dimensional texture or image.
    D2,
    /// A three-dimensional (volume) texture or image.
    D3,
    /// A cube map (six faces, sampled by direction).
    Cube,
}

/// What a texture returns when it is read.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SampleType {
    /// Floating-point (or normalized) texels, read as floats.
    Float,
    /// Signed integer texels.
    Sint,
    /// Unsigned integer texels.
    Uint,
    /// A depth texture (a source whose API distinguishes it from a float texture; D3D does not).
    Depth,
}

/// How a buffer's elements are addressed.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BufferKind {
    /// Elements of one value type (a formatted view).
    Typed(Value),
    /// Elements of this many bytes.
    /// Elements of `stride` bytes each (a D3D structured buffer's element size).
    Structured {
        /// Bytes per element.
        stride: u32,
    },
    /// Bytes, addressed by offset.
    Raw,
}

/// What a storage resource is.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum StorageTarget {
    /// A storage image.
    Image {
        /// Its dimensionality.
        dim: TextureDim,
        /// Whether it is an array of layers.
        arrayed: bool,
        /// What a read returns.
        sample: SampleType,
    },
    /// A storage buffer.
    Buffer(BufferKind),
}

/// What a program may do to a storage resource.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Access {
    /// The program only reads it.
    Read,
    /// The program only writes it.
    Write,
    /// The program reads and writes it.
    ReadWrite,
}

/// Whether consecutive lines of a matrix are its rows or its columns.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Majorness {
    /// Each row is a line (HLSL `row_major`, GLSL `row_major`).
    Row,
    /// Each column is a line (HLSL's default `column_major`, GLSL's default).
    Column,
}

/// The type of one constant, with the layout it has in its buffer written out: vectors are tightly packed lanes,
/// every matrix line and array element sits at a stated stride, whatever packing rules (D3D, std140, std430, DX9
/// registers) put it there.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ConstantType {
    /// A single value.
    Scalar(ScalarType),
    /// Consecutive lanes of one scalar type with no padding between them.
    Vector(Value),
    /// `rows` by `columns` of `scalar`. The matrix is stored as lines (rows for [`Majorness::Row`], columns for
    /// [`Majorness::Column`]), `stride` bytes apart; each line's lanes are consecutive.
    Matrix {
        /// The scalar type of every lane.
        scalar: ScalarType,
        /// Number of rows.
        rows: u8,
        /// Number of columns.
        columns: u8,
        /// Whether the stored lines are the rows or the columns.
        major: Majorness,
        /// Bytes from the start of one stored line to the start of the next.
        stride: u32,
    },
    /// A run of equal elements.
    Array {
        /// The element type.
        element: Box<ConstantType>,
        /// Number of elements.
        count: u32,
        /// Bytes from the start of one element to the start of the next.
        stride: u32,
    },
    /// Named members at explicit offsets.
    Struct {
        /// The members, in declaration order.
        members: Vec<Member>,
    },
}

/// A structure's member: its offset is from the start of the structure.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
pub struct Member {
    /// The member's name.
    pub name: String,
    /// Byte offset from the start of the structure.
    pub offset: u32,
    /// The member's type.
    pub ty: ConstantType,
}

impl ConstantType {
    /// Bytes from the start of a value of this type to the end of its last byte of data: no padding after the last
    /// matrix line, array element or member (a buffer whose rules pad an array's tail states that in the
    /// buffer's size). A value with no data (a matrix of no lines or lanes, an array of no elements) is 0. `None` if the
    /// layout overflows 32 bits.
    pub fn size(&self) -> Option<u32> {
        match self {
            Self::Scalar(s) => Some(s.bytes()),
            Self::Vector(v) => u32::from(v.width).checked_mul(v.scalar.bytes()),
            Self::Matrix {
                scalar,
                rows,
                columns,
                major,
                stride,
            } => {
                let (lines, lanes) = match major {
                    Majorness::Row => (*rows, *columns),
                    Majorness::Column => (*columns, *rows),
                };
                if lines == 0 || lanes == 0 {
                    return Some(0);
                }
                u32::from(lines.saturating_sub(1))
                    .checked_mul(*stride)?
                    .checked_add(u32::from(lanes).checked_mul(scalar.bytes())?)
            }
            Self::Array {
                element,
                count,
                stride,
            } => match count.checked_sub(1) {
                None => Some(0),
                Some(before) => before.checked_mul(*stride)?.checked_add(element.size()?),
            },
            Self::Struct { members } => members
                .iter()
                .try_fold(0u32, |end, m| Some(end.max(m.offset.checked_add(m.ty.size()?)?))),
        }
    }

    /// The members of a structure, or of the structure an array holds (those of its first element): empty for any
    /// other type.
    pub fn members(&self) -> &[Member] {
        match self {
            Self::Struct { members } => members,
            Self::Array { element, .. } => element.members(),
            _ => &[],
        }
    }
}

/// One named constant of a buffer.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
pub struct Constant {
    /// The constant's name in the source.
    pub name: String,
    /// Byte offset in the buffer.
    pub offset: u32,
    /// The constant's type and layout.
    pub ty: ConstantType,
    /// The default value the source states, as bytes of the constant's layout.
    pub default: Option<Vec<u8>>,
}

/// A constant buffer's contents.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
pub struct BufferLayout {
    /// Bytes.
    pub size: u32,
    /// The constants the source names; their offsets can leave gaps.
    pub constants: Vec<Constant>,
}

/// What a resource is.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ResourceKind {
    /// Constants by explicit byte offset. A program whose source states no names (a stripped one) has none here;
    /// the consumer names fields by offset.
    /// The constants and size of one constant buffer.
    ConstantBuffer {
        /// The buffer's size and the constants in it.
        layout: BufferLayout,
    },
    /// A texture read by sampling or by load.
    Texture {
        /// Its dimensionality.
        dim: TextureDim,
        /// Whether it is an array of layers.
        arrayed: bool,
        /// Whether it has multiple samples per texel.
        multisampled: bool,
        /// What a read returns.
        sample: SampleType,
    },
    /// A sampler state object.
    Sampler {
        /// A comparison sampler (shadow lookups).
        comparison: bool,
    },
    /// A buffer the program reads.
    Buffer(BufferKind),
    /// An image or buffer the program may write.
    /// A resource the program may write: an image or a buffer, and what it may do to it.
    Storage {
        /// Whether it is an image or a buffer, and which kind.
        target: StorageTarget,
        /// What the program does with it.
        access: Access,
    },
    /// A ray tracing acceleration structure the program traces rays into.
    AccelerationStructure,
}

/// The register class of a D3D binding: `b`, `t`, `s` or `u`.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum D3dClass {
    /// `b<n>`
    ConstantBuffer,
    /// `t<n>`
    ShaderResource,
    /// `s<n>`
    Sampler,
    /// `u<n>`
    UnorderedAccess,
}

/// A D3D bind point: class, register index and register space.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
pub struct D3dBinding {
    /// Which register class (`b`, `t`, `s` or `u`) the resource is bound in.
    pub class: D3dClass,
    /// The register index within the class and space.
    pub register: u32,
    /// The register space (`space<n>`; 0 where the source has no spaces).
    pub space: u32,
}

/// The source's own name and address for a resource; one field per API that has a notion of it.
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
pub struct ResourceProvenance {
    /// The name the source gives it (`Texture2D_8`, a constant buffer's name).
    pub name: String,
    /// The D3D bind point; `None` when the source has no such address.
    pub d3d: Option<D3dBinding>,
    /// The resource this one is the hidden counter of: an unordered-access view's 32-bit counter (D3D's append and
    /// consume buffers, `IncrementCounter`), a buffer of its own in WebGPU. It has no D3D binding of its own; the
    /// renderer binds it with the view, starting from the view's initial count.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub counter_of: Option<ResourceId>,
    /// The members the source states for a buffer view: a D3D `tbuffer`'s constant-register layout or a structured
    /// buffer's single element in storage packing. For a structured buffer, size is its element stride, not the
    /// bound element count. The kind says how it is read; this says what it holds, by name and byte offset.
    /// Absent when the source does not retain the reflection needed to name its contents.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub layout: Option<BufferLayout>,
}

/// One bound resource.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
pub struct Resource {
    /// This resource's id within its interface.
    pub id: ResourceId,
    /// What the resource is.
    pub kind: ResourceKind,
    /// How many consecutive bindings it spans: 1, or the length of a resource array. [`Resource::UNBOUNDED`] is an
    /// array whose length is the bound data's, not the program's (D3D's `-1`): nothing in the program says how far
    /// an index may go.
    pub count: u32,
    /// The source's own name and address.
    pub provenance: ResourceProvenance,
}

impl Resource {
    /// The `count` of an array the program does not size.
    pub const UNBOUNDED: u32 = u32::MAX;

    /// Whether the program sizes this array no further than the bound data does.
    pub fn is_unbounded(&self) -> bool {
        self.count == Self::UNBOUNDED
    }

    /// Whether it is an array of bindings, read by an index the program computes, rather than one binding.
    pub fn is_array(&self) -> bool {
        self.count != 1
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn float(width: u8) -> ConstantType {
        ConstantType::Vector(Value {
            scalar: ScalarType::Float,
            width,
        })
    }

    /// Sizes worked out by hand: a line takes only its own lanes at the end, an array's last element only its own.
    #[test]
    fn sizes_end_at_the_last_byte_of_data() {
        assert_eq!(ConstantType::Scalar(ScalarType::Double).size(), Some(8));
        assert_eq!(float(3).size(), Some(12));
        let matrix = |rows, columns, major| ConstantType::Matrix {
            scalar: ScalarType::Float,
            rows,
            columns,
            major,
            stride: 16,
        };
        // 3 rows of 3 lanes: two full 16-byte lines and 12 bytes
        assert_eq!(matrix(3, 3, Majorness::Row).size(), Some(44));
        // 3 columns of 4 lanes: two full lines and 16 bytes
        assert_eq!(matrix(4, 3, Majorness::Column).size(), Some(48));
        let offsets = ConstantType::Array {
            element: Box::new(float(2)),
            count: 3,
            stride: 16,
        };
        assert_eq!(offsets.size(), Some(40));
        let empty = ConstantType::Array {
            element: Box::new(float(2)),
            count: 0,
            stride: 16,
        };
        assert_eq!(empty.size(), Some(0));
        // no lines or no lanes is no data, as an empty array: 0, not an overflow
        assert_eq!(matrix(0, 3, Majorness::Row).size(), Some(0));
        assert_eq!(matrix(3, 0, Majorness::Row).size(), Some(0));
        let record = ConstantType::Struct {
            members: vec![
                Member {
                    name: "a".into(),
                    offset: 0,
                    ty: float(3),
                },
                Member {
                    name: "b".into(),
                    offset: 12,
                    ty: ConstantType::Scalar(ScalarType::Float),
                },
                Member {
                    name: "c".into(),
                    offset: 16,
                    ty: offsets,
                },
            ],
        };
        assert_eq!(record.size(), Some(56));
        let overflow = ConstantType::Array {
            element: Box::new(float(4)),
            count: u32::MAX,
            stride: 16,
        };
        assert_eq!(overflow.size(), None);
    }

    #[test]
    fn members_are_found_through_arrays_of_structures() {
        let inner = ConstantType::Struct {
            members: vec![Member {
                name: "x".into(),
                offset: 0,
                ty: float(1),
            }],
        };
        let array = ConstantType::Array {
            element: Box::new(inner),
            count: 2,
            stride: 16,
        };
        assert_eq!(array.members().len(), 1);
        assert!(float(4).members().is_empty());
    }

    #[test]
    fn a_resource_serializes_by_kind_with_its_provenance() {
        let r = Resource {
            id: ResourceId(2),
            kind: ResourceKind::Texture {
                dim: TextureDim::D2,
                arrayed: false,
                multisampled: false,
                sample: SampleType::Float,
            },
            count: 1,
            provenance: ResourceProvenance {
                name: "Texture2D_8".into(),
                d3d: Some(D3dBinding {
                    class: D3dClass::ShaderResource,
                    register: 8,
                    space: 0,
                }),
                counter_of: None,
                layout: None,
            },
        };
        assert_eq!(
            serde_json::to_value(&r).unwrap(),
            serde_json::json!({
                "id": 2,
                "kind": {"texture": {"dim": "d2", "arrayed": false, "multisampled": false, "sample": "float"}},
                "count": 1,
                "provenance": {
                    "name": "Texture2D_8",
                    "d3d": {"class": "shader_resource", "register": 8, "space": 0},
                },
            })
        );
    }
}