use serde::Serialize;
use super::{BufferKind, BufferLayout, D3dClass, Resource, ResourceId, ResourceKind, Stage, StorageTarget};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ScalarType {
Bool,
Sint,
Uint,
Float,
Double,
}
impl ScalarType {
pub fn bytes(self) -> u32 {
match self {
Self::Double => 8,
_ => 4,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
pub struct Value {
pub scalar: ScalarType,
pub width: u8,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
pub struct Label {
pub name: String,
pub index: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SystemValue {
ClipPosition,
FragCoord,
FrontFacing,
VertexIndex,
InstanceIndex,
PrimitiveIndex,
SampleIndex,
RenderTargetArrayIndex,
ViewportArrayIndex,
ClipDistance,
CullDistance,
Target(u32),
Depth,
DepthGreaterEqual,
DepthLessEqual,
StencilRef,
Coverage,
InnerCoverage,
GroupId,
GroupThreadId,
DispatchThreadId,
GroupIndex,
LaneIndex,
LaneCount,
DispatchRaysIndex,
DispatchRaysDimensions,
WorldRayOrigin,
WorldRayDirection,
ObjectRayOrigin,
ObjectRayDirection,
RayTMin,
RayTCurrent,
RayFlags,
InstanceId,
GeometryIndex,
HitKind,
ObjectToWorld,
WorldToObject,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum InterpMode {
Perspective,
Linear,
Flat,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum InterpSampling {
Center,
Centroid,
Sample,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
pub struct Interpolation {
pub mode: InterpMode,
pub sampling: InterpSampling,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
pub struct D3dSignature {
pub register: u32,
pub mask: u8,
pub read_write_mask: u8,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
pub struct VaryingProvenance {
pub d3d: Option<D3dSignature>,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
pub struct Varying {
pub label: Label,
pub system: Option<SystemValue>,
pub value: Value,
pub interp: Option<Interpolation>,
pub used: bool,
pub provenance: VaryingProvenance,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum TessDomain {
Isoline,
Triangle,
Quad,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Partitioning {
Integer,
Pow2,
FractionalOdd,
FractionalEven,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum TessTopology {
Point,
Line,
TriangleCw,
TriangleCcw,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
pub struct HullFacts {
pub domain: TessDomain,
pub partitioning: Partitioning,
pub topology: TessTopology,
pub input_control_points: u32,
pub output_control_points: u32,
pub max_tess_factor_bits: u32,
pub patch_constants: Vec<Varying>,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
pub struct DomainFacts {
pub domain: TessDomain,
pub input_control_points: u32,
pub patch_constants: Vec<Varying>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum GsInput {
Point,
Line,
Triangle,
LineAdjacency,
TriangleAdjacency,
}
impl GsInput {
pub fn vertices(self) -> u32 {
match self {
Self::Point => 1,
Self::Line => 2,
Self::Triangle => 3,
Self::LineAdjacency => 4,
Self::TriangleAdjacency => 6,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum GsOutput {
PointList,
LineStrip,
TriangleStrip,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
pub struct GeometryFacts {
pub input: GsInput,
pub output: GsOutput,
pub max_vertices: u32,
pub instances: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum MeshTopology {
Line,
Triangle,
}
impl MeshTopology {
pub fn indices(self) -> u32 {
match self {
Self::Line => 2,
Self::Triangle => 3,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
pub struct MeshFacts {
pub topology: MeshTopology,
pub max_vertices: u32,
pub max_primitives: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum HitGroupKind {
Triangles,
ProceduralPrimitives,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
pub struct HitGroup {
pub name: String,
pub kind: HitGroupKind,
pub any_hit: Option<String>,
pub closest_hit: Option<String>,
pub intersection: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Subobject {
StateObjectConfig {
name: String,
flags: u32,
},
GlobalRootSignature {
name: String,
signature: Vec<u8>,
},
LocalRootSignature {
name: String,
signature: Vec<u8>,
},
ExportsAssociation {
name: String,
subobject: String,
exports: Vec<String>,
},
ShaderConfig {
name: String,
max_payload_bytes: u32,
max_attribute_bytes: u32,
},
PipelineConfig {
name: String,
max_recursion_depth: u32,
flags: Option<u32>,
},
HitGroup(HitGroup),
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize)]
pub struct RayTracingFacts {
pub export: String,
pub subobjects: Vec<Subobject>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum StageFacts {
#[default]
None,
VertexWithoutPosition,
Hull(HullFacts),
Domain(DomainFacts),
Geometry(GeometryFacts),
RayTracing(RayTracingFacts),
Mesh(MeshFacts),
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
pub struct Interface {
pub stage: Stage,
pub inputs: Vec<Varying>,
pub outputs: Vec<Varying>,
pub resources: Vec<Resource>,
pub facts: StageFacts,
}
impl Interface {
fn verify_facts(&self) -> Result<(), String> {
let patch = |what: &str, count: u32, constants: &[Varying]| -> Result<(), String> {
if !(1..=32).contains(&count) {
return Err(format!("{what}: {count} control points, not 1 to 32"));
}
for (i, v) in constants.iter().enumerate() {
if !(1..=4).contains(&v.value.width) {
return Err(format!(
"patch constant {}{} has a width of {}, not 1 to 4",
v.label.name, v.label.index, v.value.width
));
}
if constants.iter().take(i).any(|other| other.label == v.label) {
return Err(format!(
"patch constant {}{} is listed twice",
v.label.name, v.label.index
));
}
}
Ok(())
};
match (&self.stage, &self.facts) {
(Stage::Hull, StageFacts::Hull(h)) => {
patch("hull input", h.input_control_points, &h.patch_constants)?;
patch("hull output", h.output_control_points, &[])?;
let factor = f32::from_bits(h.max_tess_factor_bits);
if !(1.0..=64.0).contains(&factor) {
return Err(format!("a maximum tessellation factor of {factor}"));
}
Ok(())
}
(Stage::Domain, StageFacts::Domain(d)) => {
patch("domain", d.input_control_points, &d.patch_constants)
}
(Stage::Geometry, StageFacts::Geometry(g)) => {
if !(1..=1024).contains(&g.max_vertices) || !(1..=32).contains(&g.instances) {
return Err(format!(
"a geometry program of {} vertices and {} instances",
g.max_vertices, g.instances
));
}
Ok(())
}
(Stage::Vertex, StageFacts::VertexWithoutPosition) => Ok(()),
(Stage::Hull | Stage::Domain, _) => {
Err(format!("a {:?} program without its patch facts", self.stage))
}
(Stage::Mesh, StageFacts::Mesh(m)) => {
if !(1..=256).contains(&m.max_vertices) || !(1..=256).contains(&m.max_primitives) {
return Err(format!(
"a mesh program of {} vertices and {} primitives",
m.max_vertices, m.max_primitives
));
}
Ok(())
}
(Stage::Mesh, _) => Err("a mesh program without its facts".into()),
(stage, StageFacts::RayTracing(r)) if stage.is_ray_tracing() => {
if r.export.is_empty() {
return Err(format!("a {stage:?} program that names no export"));
}
Ok(())
}
(_, StageFacts::None) => Ok(()),
(stage, facts) => Err(format!("{stage:?} program with {facts:?}")),
}
}
pub fn resource(&self, id: ResourceId) -> Option<&Resource> {
self.resources.iter().find(|r| r.id == id)
}
pub fn verify(&self) -> Result<(), String> {
self.verify_facts()?;
for (direction, varyings) in [("input", &self.inputs), ("output", &self.outputs)] {
for (i, v) in varyings.iter().enumerate() {
if let Some(SystemValue::Target(n)) = v.system {
if direction == "input" || self.stage != Stage::Pixel {
return Err(format!(
"{direction} {}{} is colour output {n} of a {:?} stage: only a pixel stage's outputs are",
v.label.name, v.label.index, self.stage
));
}
if n > 7 {
return Err(format!(
"output {}{} is colour output {n}, not 0 to 7",
v.label.name, v.label.index
));
}
}
if !(1..=4).contains(&v.value.width) {
return Err(format!(
"{direction} {}{} has a width of {}, not 1 to 4",
v.label.name, v.label.index, v.value.width
));
}
if varyings.iter().take(i).any(|other| other.label == v.label) {
return Err(format!(
"{direction} {}{} is listed twice",
v.label.name, v.label.index
));
}
}
}
let width_ok = |w: u8| (1..=4).contains(&w);
for (i, r) in self.resources.iter().enumerate() {
let name = &r.provenance.name;
if self.resources.iter().take(i).any(|other| other.id == r.id) {
return Err(format!("resource id {} ({name}) is used twice", r.id.0));
}
if r.count == 0 {
return Err(format!("resource {name} spans no bindings"));
}
let buffer = |kind: &BufferKind| match kind {
BufferKind::Typed(v) if !width_ok(v.width) => {
Err(format!("buffer {name} has elements of {} lanes", v.width))
}
BufferKind::Structured { stride: 0 } => Err(format!("buffer {name} has a stride of 0")),
BufferKind::Structured { stride } if !stride.is_multiple_of(4) || *stride > 2048 => Err(
format!("buffer {name} has a stride of {stride}, not a multiple of 4 up to 2048"),
),
_ => Ok(()),
};
match &r.kind {
ResourceKind::Buffer(kind)
| ResourceKind::Storage {
target: StorageTarget::Buffer(kind),
..
} => {
buffer(kind)?;
}
ResourceKind::ConstantBuffer { layout } => layout_fits(name, layout)?,
_ => {}
}
if let Some(layout) = &r.provenance.layout {
layout_fits(name, layout)?;
}
if let Some(target) = r.provenance.counter_of {
let owner = self.resource(target).ok_or_else(|| {
format!(
"resource {name} is the counter of resource {}, which is not there",
target.0
)
})?;
if target == r.id || !matches!(owner.kind, ResourceKind::Storage { .. }) {
return Err(format!(
"resource {name} is the counter of resource {} ({}), which is not another storage resource",
target.0, owner.provenance.name
));
}
if r.provenance.d3d.is_some() {
return Err(format!(
"resource {name} is a hidden counter and has a binding of its own"
));
}
}
if let Some(d3d) = r.provenance.d3d {
let class = match &r.kind {
ResourceKind::ConstantBuffer { .. } => D3dClass::ConstantBuffer,
ResourceKind::Texture { .. }
| ResourceKind::Buffer(_)
| ResourceKind::AccelerationStructure => D3dClass::ShaderResource,
ResourceKind::Sampler { .. } => D3dClass::Sampler,
ResourceKind::Storage { .. } => D3dClass::UnorderedAccess,
};
if d3d.class != class {
return Err(format!(
"resource {name} is bound as {:?} but its kind is read through {class:?}",
d3d.class
));
}
}
}
let span = |r: &Resource| {
r.provenance.d3d.map(|d| {
let first = u64::from(d.register);
let end = if r.is_unbounded() {
u64::MAX
} else {
first.saturating_add(u64::from(r.count))
};
(d.class, d.space, first, end)
})
};
for (i, a) in self.resources.iter().enumerate() {
let Some((class, space, first, end)) = span(a) else {
continue;
};
for b in self.resources.iter().take(i) {
if let Some((c, s, f, e)) = span(b)
&& (c, s) == (class, space)
&& first < e
&& f < end
{
return Err(format!(
"resources {} and {} share a {class:?} register of space {space}",
b.provenance.name, a.provenance.name
));
}
}
}
Ok(())
}
}
fn layout_fits(name: &str, layout: &BufferLayout) -> Result<(), String> {
for c in &layout.constants {
let size =
c.ty.size()
.ok_or_else(|| format!("constant {name}.{} overflows", c.name))?;
if c.offset.checked_add(size).is_none_or(|end| end > layout.size) {
return Err(format!(
"constant {name}.{} ({size} bytes at {}) reaches past the end of the {}-byte buffer",
c.name, c.offset, layout.size
));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
BufferLayout, Constant, ConstantType, D3dBinding, ResourceProvenance, SampleType, TextureDim,
};
fn pixel() -> Interface {
Interface {
stage: Stage::Pixel,
inputs: Vec::new(),
outputs: Vec::new(),
resources: Vec::new(),
facts: Default::default(),
}
}
fn target(index: u32, system: Option<SystemValue>) -> Varying {
Varying {
label: Label {
name: "SV_Target".into(),
index,
},
system,
value: Value {
scalar: ScalarType::Float,
width: 4,
},
interp: None,
used: true,
provenance: Default::default(),
}
}
fn texture() -> ResourceKind {
ResourceKind::Texture {
dim: TextureDim::D2,
arrayed: false,
multisampled: false,
sample: SampleType::Float,
}
}
fn res(
id: u32,
name: &str,
kind: ResourceKind,
d3d: Option<(D3dClass, u32, u32)>,
count: u32,
) -> Resource {
Resource {
id: ResourceId(id),
kind,
count,
provenance: ResourceProvenance {
name: name.into(),
d3d: d3d.map(|(class, register, space)| D3dBinding {
class,
register,
space,
}),
..ResourceProvenance::default()
},
}
}
fn storage() -> ResourceKind {
ResourceKind::Storage {
target: StorageTarget::Buffer(BufferKind::Raw),
access: crate::Access::ReadWrite,
}
}
fn broken(i: &Interface, expect: &str) {
let message = i.verify().unwrap_err();
assert!(message.contains(expect), "{expect}: {message}");
}
#[test]
fn a_colour_output_is_a_pixel_outputs_target_of_0_to_7() {
let mut i = pixel();
i.outputs = vec![
target(0, Some(SystemValue::Target(0))),
target(7, Some(SystemValue::Target(7))),
];
assert_eq!(i.verify(), Ok(()));
i.outputs.push(target(8, Some(SystemValue::Target(8))));
broken(&i, "colour output 8, not 0 to 7");
let mut i = pixel();
i.inputs = vec![target(0, Some(SystemValue::Target(0)))];
broken(&i, "only a pixel stage's outputs are");
let mut i = pixel();
i.stage = Stage::Vertex;
i.outputs = vec![target(0, Some(SystemValue::Target(0)))];
broken(&i, "only a pixel stage's outputs are");
}
#[test]
fn a_structured_buffers_stride_is_a_multiple_of_4_up_to_2048() {
for (stride, ok) in [
(4, true),
(16, true),
(2048, true),
(0, false),
(6, false),
(2052, false),
] {
let mut i = pixel();
i.resources = vec![res(
0,
"b",
ResourceKind::Buffer(BufferKind::Structured { stride }),
Some((D3dClass::ShaderResource, 0, 0)),
1,
)];
assert_eq!(i.verify().is_ok(), ok, "stride {stride}");
}
}
#[test]
fn a_tbuffers_members_lie_inside_its_layout() {
let layout = |offset| BufferLayout {
size: 16,
constants: vec![Constant {
name: "m".into(),
offset,
ty: ConstantType::Vector(Value {
scalar: ScalarType::Float,
width: 4,
}),
default: None,
}],
};
let mut view = res(
0,
"tb",
ResourceKind::Buffer(BufferKind::Typed(Value {
scalar: ScalarType::Float,
width: 4,
})),
Some((D3dClass::ShaderResource, 0, 0)),
1,
);
view.provenance.layout = Some(layout(0));
let mut i = pixel();
i.resources = vec![view.clone()];
assert_eq!(i.verify(), Ok(()));
i.resources[0].provenance.layout = Some(layout(4));
broken(&i, "reaches past the end");
}
#[test]
fn a_hidden_counter_belongs_to_a_storage_resource_and_has_no_binding() {
let counter = |of: u32, d3d| {
let mut c = res(1, "c", ResourceKind::Buffer(BufferKind::Raw), d3d, 1);
c.provenance.counter_of = Some(ResourceId(of));
c
};
let uav = res(0, "u", storage(), Some((D3dClass::UnorderedAccess, 0, 0)), 1);
let mut i = pixel();
i.resources = vec![uav.clone(), counter(0, None)];
assert_eq!(i.verify(), Ok(()));
i.resources = vec![uav.clone(), counter(5, None)];
broken(&i, "which is not there");
i.resources = vec![uav.clone(), counter(1, None)];
broken(&i, "not another storage resource");
i.resources = vec![
res(0, "t", texture(), Some((D3dClass::ShaderResource, 0, 0)), 1),
counter(0, None),
];
broken(&i, "not another storage resource");
i.resources = vec![uav, counter(0, Some((D3dClass::ShaderResource, 3, 0)))];
broken(&i, "binding of its own");
}
#[test]
fn a_register_class_fits_the_kind_and_a_register_holds_one_resource() {
let cb = ResourceKind::ConstantBuffer {
layout: BufferLayout {
size: 16,
constants: Vec::new(),
},
};
let kinds = [
(cb.clone(), D3dClass::ConstantBuffer),
(texture(), D3dClass::ShaderResource),
(ResourceKind::Buffer(BufferKind::Raw), D3dClass::ShaderResource),
(ResourceKind::AccelerationStructure, D3dClass::ShaderResource),
(ResourceKind::Sampler { comparison: false }, D3dClass::Sampler),
(storage(), D3dClass::UnorderedAccess),
];
for (kind, class) in &kinds {
let mut i = pixel();
i.resources = vec![res(0, "r", kind.clone(), Some((*class, 0, 0)), 1)];
assert_eq!(i.verify(), Ok(()), "{kind:?}");
for (_, other) in kinds.iter().filter(|(_, c)| c != class) {
i.resources = vec![res(0, "r", kind.clone(), Some((*other, 0, 0)), 1)];
broken(&i, "its kind is read through");
}
}
let t = |id, register, space, count| {
res(
id,
&format!("t{id}"),
texture(),
Some((D3dClass::ShaderResource, register, space)),
count,
)
};
let mut i = pixel();
i.resources = vec![t(0, 2, 0, 1), t(1, 2, 1, 1), t(2, 3, 0, 1)];
assert_eq!(i.verify(), Ok(()));
i.resources.push(res(
3,
"s",
ResourceKind::Sampler { comparison: false },
Some((D3dClass::Sampler, 2, 0)),
1,
));
assert_eq!(i.verify(), Ok(()));
i.resources = vec![t(0, 2, 0, 1), t(1, 2, 0, 1)];
broken(&i, "share a ShaderResource register of space 0");
i.resources = vec![t(0, 2, 0, 4), t(1, 6, 0, 1)];
assert_eq!(i.verify(), Ok(()));
i.resources = vec![t(0, 2, 0, 4), t(1, 6, 0, 1), t(2, 5, 0, 1)];
broken(&i, "t0 and t2 share");
i.resources = vec![t(0, 2, 0, Resource::UNBOUNDED), t(1, 90, 0, 1)];
broken(&i, "share a ShaderResource");
i.resources = vec![t(0, 2, 0, Resource::UNBOUNDED), t(1, 1, 0, 1), t(2, 2, 1, 1)];
assert_eq!(i.verify(), Ok(()));
}
#[test]
fn a_varying_serializes_with_its_provenance() {
let v = Varying {
label: Label {
name: "TEXCOORD".into(),
index: 5,
},
system: None,
value: Value {
scalar: ScalarType::Float,
width: 2,
},
interp: Some(Interpolation {
mode: InterpMode::Perspective,
sampling: InterpSampling::Center,
}),
used: true,
provenance: VaryingProvenance {
d3d: Some(D3dSignature {
register: 3,
mask: 0b1100,
read_write_mask: 0b1100,
}),
},
};
assert_eq!(
serde_json::to_value(&v).unwrap(),
serde_json::json!({
"label": {"name": "TEXCOORD", "index": 5},
"system": null,
"value": {"scalar": "float", "width": 2},
"interp": {"mode": "perspective", "sampling": "center"},
"used": true,
"provenance": {"d3d": {"register": 3, "mask": 12, "read_write_mask": 12}},
})
);
assert_eq!(
serde_json::to_value(SystemValue::Target(2)).unwrap(),
serde_json::json!({"target": 2})
);
}
}