iridia_ir/interface/resource.rs
1//! A program's resources: constant buffers with explicit layouts, textures, samplers, buffers and storage.
2#[allow(unused_imports)]
3use alloc::{
4 boxed::Box,
5 format,
6 string::{String, ToString},
7 vec,
8 vec::Vec,
9};
10
11use serde::Serialize;
12
13use super::{ScalarType, Value};
14
15/// Names a resource within one [`super::Interface`]; the reader numbers them in its source's order.
16#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
17pub struct ResourceId(pub u32);
18
19/// The dimensionality of a texture or image. Whether it is an array or multisampled is separate.
20#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
21#[serde(rename_all = "snake_case")]
22pub enum TextureDim {
23 /// A one-dimensional texture or image.
24 D1,
25 /// A two-dimensional texture or image.
26 D2,
27 /// A three-dimensional (volume) texture or image.
28 D3,
29 /// A cube map (six faces, sampled by direction).
30 Cube,
31}
32
33/// What a texture returns when it is read.
34#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
35#[serde(rename_all = "snake_case")]
36pub enum SampleType {
37 /// Floating-point (or normalized) texels, read as floats.
38 Float,
39 /// Signed integer texels.
40 Sint,
41 /// Unsigned integer texels.
42 Uint,
43 /// A depth texture (a source whose API distinguishes it from a float texture; D3D does not).
44 Depth,
45}
46
47/// How a buffer's elements are addressed.
48#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
49#[serde(rename_all = "snake_case")]
50pub enum BufferKind {
51 /// Elements of one value type (a formatted view).
52 Typed(Value),
53 /// Elements of this many bytes.
54 /// Elements of `stride` bytes each (a D3D structured buffer's element size).
55 Structured {
56 /// Bytes per element.
57 stride: u32,
58 },
59 /// Bytes, addressed by offset.
60 Raw,
61}
62
63/// What a storage resource is.
64#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
65#[serde(rename_all = "snake_case")]
66pub enum StorageTarget {
67 /// A storage image.
68 Image {
69 /// Its dimensionality.
70 dim: TextureDim,
71 /// Whether it is an array of layers.
72 arrayed: bool,
73 /// What a read returns.
74 sample: SampleType,
75 },
76 /// A storage buffer.
77 Buffer(BufferKind),
78}
79
80/// What a program may do to a storage resource.
81#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
82#[serde(rename_all = "snake_case")]
83pub enum Access {
84 /// The program only reads it.
85 Read,
86 /// The program only writes it.
87 Write,
88 /// The program reads and writes it.
89 ReadWrite,
90}
91
92/// Whether consecutive lines of a matrix are its rows or its columns.
93#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
94#[serde(rename_all = "snake_case")]
95pub enum Majorness {
96 /// Each row is a line (HLSL `row_major`, GLSL `row_major`).
97 Row,
98 /// Each column is a line (HLSL's default `column_major`, GLSL's default).
99 Column,
100}
101
102/// The type of one constant, with the layout it has in its buffer written out: vectors are tightly packed lanes,
103/// every matrix line and array element sits at a stated stride, whatever packing rules (D3D, std140, std430, DX9
104/// registers) put it there.
105#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
106#[serde(rename_all = "snake_case")]
107pub enum ConstantType {
108 /// A single value.
109 Scalar(ScalarType),
110 /// Consecutive lanes of one scalar type with no padding between them.
111 Vector(Value),
112 /// `rows` by `columns` of `scalar`. The matrix is stored as lines (rows for [`Majorness::Row`], columns for
113 /// [`Majorness::Column`]), `stride` bytes apart; each line's lanes are consecutive.
114 Matrix {
115 /// The scalar type of every lane.
116 scalar: ScalarType,
117 /// Number of rows.
118 rows: u8,
119 /// Number of columns.
120 columns: u8,
121 /// Whether the stored lines are the rows or the columns.
122 major: Majorness,
123 /// Bytes from the start of one stored line to the start of the next.
124 stride: u32,
125 },
126 /// A run of equal elements.
127 Array {
128 /// The element type.
129 element: Box<ConstantType>,
130 /// Number of elements.
131 count: u32,
132 /// Bytes from the start of one element to the start of the next.
133 stride: u32,
134 },
135 /// Named members at explicit offsets.
136 Struct {
137 /// The members, in declaration order.
138 members: Vec<Member>,
139 },
140}
141
142/// A structure's member: its offset is from the start of the structure.
143#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
144pub struct Member {
145 /// The member's name.
146 pub name: String,
147 /// Byte offset from the start of the structure.
148 pub offset: u32,
149 /// The member's type.
150 pub ty: ConstantType,
151}
152
153impl ConstantType {
154 /// Bytes from the start of a value of this type to the end of its last byte of data: no padding after the last
155 /// matrix line, array element or member (a buffer whose rules pad an array's tail states that in the
156 /// 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
157 /// layout overflows 32 bits.
158 pub fn size(&self) -> Option<u32> {
159 match self {
160 Self::Scalar(s) => Some(s.bytes()),
161 Self::Vector(v) => u32::from(v.width).checked_mul(v.scalar.bytes()),
162 Self::Matrix {
163 scalar,
164 rows,
165 columns,
166 major,
167 stride,
168 } => {
169 let (lines, lanes) = match major {
170 Majorness::Row => (*rows, *columns),
171 Majorness::Column => (*columns, *rows),
172 };
173 if lines == 0 || lanes == 0 {
174 return Some(0);
175 }
176 u32::from(lines.saturating_sub(1))
177 .checked_mul(*stride)?
178 .checked_add(u32::from(lanes).checked_mul(scalar.bytes())?)
179 }
180 Self::Array {
181 element,
182 count,
183 stride,
184 } => match count.checked_sub(1) {
185 None => Some(0),
186 Some(before) => before.checked_mul(*stride)?.checked_add(element.size()?),
187 },
188 Self::Struct { members } => members
189 .iter()
190 .try_fold(0u32, |end, m| Some(end.max(m.offset.checked_add(m.ty.size()?)?))),
191 }
192 }
193
194 /// The members of a structure, or of the structure an array holds (those of its first element): empty for any
195 /// other type.
196 pub fn members(&self) -> &[Member] {
197 match self {
198 Self::Struct { members } => members,
199 Self::Array { element, .. } => element.members(),
200 _ => &[],
201 }
202 }
203}
204
205/// One named constant of a buffer.
206#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
207pub struct Constant {
208 /// The constant's name in the source.
209 pub name: String,
210 /// Byte offset in the buffer.
211 pub offset: u32,
212 /// The constant's type and layout.
213 pub ty: ConstantType,
214 /// The default value the source states, as bytes of the constant's layout.
215 pub default: Option<Vec<u8>>,
216}
217
218/// A constant buffer's contents.
219#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
220pub struct BufferLayout {
221 /// Bytes.
222 pub size: u32,
223 /// The constants the source names; their offsets can leave gaps.
224 pub constants: Vec<Constant>,
225}
226
227/// What a resource is.
228#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
229#[serde(rename_all = "snake_case")]
230pub enum ResourceKind {
231 /// Constants by explicit byte offset. A program whose source states no names (a stripped one) has none here;
232 /// the consumer names fields by offset.
233 /// The constants and size of one constant buffer.
234 ConstantBuffer {
235 /// The buffer's size and the constants in it.
236 layout: BufferLayout,
237 },
238 /// A texture read by sampling or by load.
239 Texture {
240 /// Its dimensionality.
241 dim: TextureDim,
242 /// Whether it is an array of layers.
243 arrayed: bool,
244 /// Whether it has multiple samples per texel.
245 multisampled: bool,
246 /// What a read returns.
247 sample: SampleType,
248 },
249 /// A sampler state object.
250 Sampler {
251 /// A comparison sampler (shadow lookups).
252 comparison: bool,
253 },
254 /// A buffer the program reads.
255 Buffer(BufferKind),
256 /// An image or buffer the program may write.
257 /// A resource the program may write: an image or a buffer, and what it may do to it.
258 Storage {
259 /// Whether it is an image or a buffer, and which kind.
260 target: StorageTarget,
261 /// What the program does with it.
262 access: Access,
263 },
264 /// A ray tracing acceleration structure the program traces rays into.
265 AccelerationStructure,
266 /// Fixed-function pipeline state the code depends on, set by the host with the draw rather than in a buffer.
267 PipelineState(StateParam),
268}
269
270/// A piece of pipeline state a program reads (`Op::LoadState`), by the stage or unit it belongs to. A known one is
271/// folded into literals by specialisation; an unknown numeric one is carried in a state block the binding plan
272/// places; an unknown one that changes types (a texture's dimension) has no uniform form.
273#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
274#[serde(rename_all = "snake_case")]
275pub enum StateParam {
276 /// Whether texture stage `n` divides its coordinate by its last lane before sampling (D3D9's
277 /// `D3DTTFF_PROJECTED`): a `bool`.
278 Projected(u32),
279 /// The dimension of the texture bound to stage `n` (what D3D9's `ps_1_x` `tex` samples): a `u32`, the
280 /// [`TextureDim::state_code`] of it. It changes the type of the sample, so only specialisation lowers it.
281 TextureDim(u32),
282 /// Stage `n`'s bump environment matrix (D3D9's `D3DTSS_BUMPENVMAT00`, `01`, `10`, `11`, in that order): a
283 /// `vec4<f32>`.
284 BumpEnvMat(u32),
285 /// Stage `n`'s bump luminance scale (D3D9's `D3DTSS_BUMPENVLSCALE`): an `f32`.
286 BumpLScale(u32),
287 /// Stage `n`'s bump luminance offset (D3D9's `D3DTSS_BUMPENVLOFFSET`): an `f32`.
288 BumpLOffset(u32),
289}
290
291impl StateParam {
292 /// The value the program reads.
293 pub fn value(self) -> Value {
294 let (scalar, width) = match self {
295 StateParam::Projected(_) => (ScalarType::Bool, 1),
296 StateParam::TextureDim(_) => (ScalarType::Uint, 1),
297 StateParam::BumpEnvMat(_) => (ScalarType::Float, 4),
298 StateParam::BumpLScale(_) | StateParam::BumpLOffset(_) => (ScalarType::Float, 1),
299 };
300 Value { scalar, width }
301 }
302}
303
304impl TextureDim {
305 /// The value [`StateParam::TextureDim`] reads for this dimension: 1, 2 and 3 for 1D, 2D and volume, 4 for a cube.
306 pub fn state_code(self) -> u32 {
307 match self {
308 TextureDim::D1 => 1,
309 TextureDim::D2 => 2,
310 TextureDim::D3 => 3,
311 TextureDim::Cube => 4,
312 }
313 }
314}
315
316/// The register class of a D3D binding: `b`, `t`, `s` or `u`.
317#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
318#[serde(rename_all = "snake_case")]
319pub enum D3dClass {
320 /// `b<n>`
321 ConstantBuffer,
322 /// `t<n>`
323 ShaderResource,
324 /// `s<n>`
325 Sampler,
326 /// `u<n>`
327 UnorderedAccess,
328}
329
330/// A D3D bind point: class, register index and register space.
331#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
332pub struct D3dBinding {
333 /// Which register class (`b`, `t`, `s` or `u`) the resource is bound in.
334 pub class: D3dClass,
335 /// The register index within the class and space.
336 pub register: u32,
337 /// The register space (`space<n>`; 0 where the source has no spaces).
338 pub space: u32,
339}
340
341/// The source's own name and address for a resource; one field per API that has a notion of it.
342#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
343pub struct ResourceProvenance {
344 /// The name the source gives it (`Texture2D_8`, a constant buffer's name).
345 pub name: String,
346 /// The D3D bind point; `None` when the source has no such address.
347 pub d3d: Option<D3dBinding>,
348 /// The resource this one is the hidden counter of: an unordered-access view's 32-bit counter (D3D's append and
349 /// consume buffers, `IncrementCounter`), a buffer of its own in WebGPU. It has no D3D binding of its own; the
350 /// renderer binds it with the view, starting from the view's initial count.
351 #[serde(skip_serializing_if = "Option::is_none")]
352 pub counter_of: Option<ResourceId>,
353 /// The members the source states for a buffer view: a D3D `tbuffer`'s constant-register layout or a structured
354 /// buffer's single element in storage packing. For a structured buffer, size is its element stride, not the
355 /// bound element count. The kind says how it is read; this says what it holds, by name and byte offset.
356 /// Absent when the source does not retain the reflection needed to name its contents.
357 #[serde(skip_serializing_if = "Option::is_none")]
358 pub layout: Option<BufferLayout>,
359}
360
361/// One bound resource.
362#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
363pub struct Resource {
364 /// This resource's id within its interface.
365 pub id: ResourceId,
366 /// What the resource is.
367 pub kind: ResourceKind,
368 /// How many consecutive bindings it spans: 1, or the length of a resource array. [`Resource::UNBOUNDED`] is an
369 /// array whose length is the bound data's, not the program's (D3D's `-1`): nothing in the program says how far
370 /// an index may go.
371 pub count: u32,
372 /// The source's own name and address.
373 pub provenance: ResourceProvenance,
374}
375
376impl Resource {
377 /// The `count` of an array the program does not size.
378 pub const UNBOUNDED: u32 = u32::MAX;
379
380 /// Whether the program sizes this array no further than the bound data does.
381 pub fn is_unbounded(&self) -> bool {
382 self.count == Self::UNBOUNDED
383 }
384
385 /// Whether it is an array of bindings, read by an index the program computes, rather than one binding.
386 pub fn is_array(&self) -> bool {
387 self.count != 1
388 }
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394
395 fn float(width: u8) -> ConstantType {
396 ConstantType::Vector(Value {
397 scalar: ScalarType::Float,
398 width,
399 })
400 }
401
402 /// Sizes worked out by hand: a line takes only its own lanes at the end, an array's last element only its own.
403 #[test]
404 fn sizes_end_at_the_last_byte_of_data() {
405 assert_eq!(ConstantType::Scalar(ScalarType::Double).size(), Some(8));
406 assert_eq!(float(3).size(), Some(12));
407 let matrix = |rows, columns, major| ConstantType::Matrix {
408 scalar: ScalarType::Float,
409 rows,
410 columns,
411 major,
412 stride: 16,
413 };
414 // 3 rows of 3 lanes: two full 16-byte lines and 12 bytes
415 assert_eq!(matrix(3, 3, Majorness::Row).size(), Some(44));
416 // 3 columns of 4 lanes: two full lines and 16 bytes
417 assert_eq!(matrix(4, 3, Majorness::Column).size(), Some(48));
418 let offsets = ConstantType::Array {
419 element: Box::new(float(2)),
420 count: 3,
421 stride: 16,
422 };
423 assert_eq!(offsets.size(), Some(40));
424 let empty = ConstantType::Array {
425 element: Box::new(float(2)),
426 count: 0,
427 stride: 16,
428 };
429 assert_eq!(empty.size(), Some(0));
430 // no lines or no lanes is no data, as an empty array: 0, not an overflow
431 assert_eq!(matrix(0, 3, Majorness::Row).size(), Some(0));
432 assert_eq!(matrix(3, 0, Majorness::Row).size(), Some(0));
433 let record = ConstantType::Struct {
434 members: vec![
435 Member {
436 name: "a".into(),
437 offset: 0,
438 ty: float(3),
439 },
440 Member {
441 name: "b".into(),
442 offset: 12,
443 ty: ConstantType::Scalar(ScalarType::Float),
444 },
445 Member {
446 name: "c".into(),
447 offset: 16,
448 ty: offsets,
449 },
450 ],
451 };
452 assert_eq!(record.size(), Some(56));
453 let overflow = ConstantType::Array {
454 element: Box::new(float(4)),
455 count: u32::MAX,
456 stride: 16,
457 };
458 assert_eq!(overflow.size(), None);
459 }
460
461 #[test]
462 fn members_are_found_through_arrays_of_structures() {
463 let inner = ConstantType::Struct {
464 members: vec![Member {
465 name: "x".into(),
466 offset: 0,
467 ty: float(1),
468 }],
469 };
470 let array = ConstantType::Array {
471 element: Box::new(inner),
472 count: 2,
473 stride: 16,
474 };
475 assert_eq!(array.members().len(), 1);
476 assert!(float(4).members().is_empty());
477 }
478
479 #[test]
480 fn a_resource_serializes_by_kind_with_its_provenance() {
481 let r = Resource {
482 id: ResourceId(2),
483 kind: ResourceKind::Texture {
484 dim: TextureDim::D2,
485 arrayed: false,
486 multisampled: false,
487 sample: SampleType::Float,
488 },
489 count: 1,
490 provenance: ResourceProvenance {
491 name: "Texture2D_8".into(),
492 d3d: Some(D3dBinding {
493 class: D3dClass::ShaderResource,
494 register: 8,
495 space: 0,
496 }),
497 counter_of: None,
498 layout: None,
499 },
500 };
501 assert_eq!(
502 serde_json::to_value(&r).unwrap(),
503 serde_json::json!({
504 "id": 2,
505 "kind": {"texture": {"dim": "d2", "arrayed": false, "multisampled": false, "sample": "float"}},
506 "count": 1,
507 "provenance": {
508 "name": "Texture2D_8",
509 "d3d": {"class": "shader_resource", "register": 8, "space": 0},
510 },
511 })
512 );
513 }
514}