1use serde::Serialize;
6use serde_json::Value;
7
8use crate::shader::{
9 BufferBinding, BufferSource, BufferTable, ConstantBinding, ConstantRole, FrameTexture, InputBinding,
10 ObjectParameter, OutputBinding, OutputRole, VariantKey,
11};
12use iridia_interface::{
13 BufferKind, ConstantBufferBinding, Interface, Label, MatrixLines, RenderState, ResourceId, ResourceKind,
14 SamplerState, ScalarType, Stage, SystemValue, Unshown,
15};
16
17use super::Shader;
18use crate::TextureLayers;
19use crate::components::{Texture, TextureFile};
20use crate::json::JsonError;
21use crate::world::{Component, Entity};
22
23#[derive(Clone, Debug, PartialEq, Serialize)]
25#[serde(rename_all = "snake_case")]
26pub enum TextureSource {
27 Texture { texture: Entity, sampler: SamplerState },
30 Default { value: [f32; 4], why: String },
32 Frame(FrameTexture),
34 Missing { why: String },
37 Target { view: crate::render_graph::TargetView },
42}
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub(crate) enum BindingPlace {
46 Material,
48 GraphStep,
50}
51#[derive(Clone, Debug, PartialEq, Serialize)]
53pub struct TextureBinding {
54 pub resource: ResourceId,
55 pub sampler: Option<ResourceId>,
59 pub source: TextureSource,
60}
61#[derive(Clone, Debug, PartialEq, Serialize)]
63pub struct SamplerBinding {
64 pub resource: ResourceId,
66 pub state: SamplerState,
68}
69#[derive(Clone, Debug, PartialEq, Serialize)]
71pub struct StageBinding {
72 pub stage: Stage,
73 pub variant: Option<VariantKey>,
78 pub shader: Entity,
80 pub inputs: Vec<InputBinding>,
81 pub constants: Vec<ConstantBinding>,
82 pub textures: Vec<TextureBinding>,
83 pub samplers: Vec<SamplerBinding>,
88 pub buffers: Vec<BufferBinding>,
93 pub constant_buffers: Vec<ConstantBufferBinding>,
96 pub outputs: Vec<OutputBinding>,
97}
98#[derive(Clone, Debug, Default, PartialEq, Serialize)]
103pub struct ShaderBinding {
104 pub stages: Vec<StageBinding>,
106 pub state: RenderState,
107}
108impl ShaderBinding {
109 pub fn stage(&self, stage: Stage) -> Option<&StageBinding> {
111 self.stages.iter().find(|s| s.stage == stage)
112 }
113}
114impl StageBinding {
115 pub fn new(stage: Stage, shader: Entity) -> StageBinding {
119 StageBinding {
120 stage,
121 variant: None,
122 shader,
123 inputs: Vec::new(),
124 constants: Vec::new(),
125 textures: Vec::new(),
126 samplers: Vec::new(),
127 buffers: Vec::new(),
128 constant_buffers: Vec::new(),
129 outputs: Vec::new(),
130 }
131 }
132
133 pub fn known_words(&self) -> std::collections::BTreeMap<(ResourceId, u32), u32> {
136 let mut out = std::collections::BTreeMap::new();
137 for constant in &self.constants {
138 let words = match &constant.role {
139 ConstantRole::Material(words) | ConstantRole::Default { words, .. } => words,
140 _ => continue,
141 };
142 for (index, word) in words.iter().enumerate() {
143 if let Ok(index) = u32::try_from(index)
144 && let Some(offset) = index.checked_mul(4)
145 && offset < constant.size
146 && let Some(offset) = constant.offset.checked_add(offset)
147 {
148 out.insert((constant.resource, offset), *word);
149 }
150 }
151 }
152 out
153 }
154
155 pub fn sampler_state(&self, resource: ResourceId) -> Option<&SamplerState> {
157 self.samplers
158 .iter()
159 .find(|s| s.resource == resource)
160 .map(|s| &s.state)
161 }
162
163 pub fn unknown_constants(&self) -> impl Iterator<Item = (String, &str)> {
167 self.constants.iter().filter_map(|c| match &c.role {
168 ConstantRole::Unknown { why } => Some((
169 c.name.clone().unwrap_or_else(|| format!("+{}", c.offset)),
170 why.as_str(),
171 )),
172 _ => None,
173 })
174 }
175
176 pub fn unbound_written_lanes<'i>(&self, interface: &'i Interface) -> Vec<(&'i Label, u8)> {
182 let mut out = Vec::new();
183 for v in &interface.outputs {
184 if v.system.is_some_and(|s| !matches!(s, SystemValue::Target(_))) {
185 continue;
186 }
187 let Some(d3d) = v.provenance.d3d else {
188 continue;
189 };
190 let first = d3d.mask.trailing_zeros();
192 let width = u32::from(v.value.width).min(4);
193 let written = ((d3d.mask & !d3d.read_write_mask) >> first) & ((1u8 << width) - 1);
194 let bound = self
195 .outputs
196 .iter()
197 .filter(|o| o.label == v.label)
198 .fold(0u8, |acc, o| acc | o.lanes);
199 let unbound = written & !bound;
200 if unbound != 0 {
201 out.push((&v.label, unbound));
202 }
203 }
204 out
205 }
206
207 pub(crate) fn verify_in(&self, w: &crate::World, place: BindingPlace) -> Result<(), String> {
210 let shader = w
211 .get::<Shader>(self.shader)
212 .ok_or_else(|| format!("the {:?} stage's shader entity has no core.shader", self.stage))?;
213 let interface: &Interface = &shader.interface;
214 let at = |detail: String| format!("{:?} stage: {detail}", self.stage);
215 if interface.stage != self.stage {
216 return Err(at(format!("its shader is a {:?} program", interface.stage)));
217 }
218 for (n, i) in self.inputs.iter().enumerate() {
219 let Some(varying) = interface.inputs.iter().find(|v| v.label == i.label) else {
220 return Err(at(format!(
221 "input {}{} is not in the shader",
222 i.label.name, i.label.index
223 )));
224 };
225 if self.inputs[..n].iter().any(|before| before.label == i.label) {
226 return Err(at(format!(
227 "input {}{} is bound twice",
228 i.label.name, i.label.index
229 )));
230 }
231 if let super::InputRole::PreviousOr { fallback, why } = &i.role {
232 if self.stage != Stage::Pixel
233 || varying.system.is_some()
234 || varying.value.scalar != ScalarType::Float
235 {
236 return Err(at("a replacement input must be a float pixel varying".into()));
237 }
238 if why.trim().is_empty() {
239 return Err(at("a replacement input has no reason".into()));
240 }
241 fallback.check().map_err(at)?;
242 }
243 }
244 for (n, o) in self.outputs.iter().enumerate() {
245 let Some(v) = interface.outputs.iter().find(|v| v.label == o.label) else {
246 return Err(at(format!(
247 "output {}{} is not in the shader",
248 o.label.name, o.label.index
249 )));
250 };
251 if o.lanes == 0 || u32::from(o.lanes) >> v.value.width != 0 {
252 return Err(at(format!(
253 "output {}{} has {} lanes, not the mask {:#x}",
254 o.label.name, o.label.index, v.value.width, o.lanes
255 )));
256 }
257 if let Some(Err(why)) = o.role.scalar_encoding().map(|e| e.check()) {
258 return Err(at(format!("output {}{}: {why}", o.label.name, o.label.index)));
259 }
260 if let OutputRole::Unknown { why } = &o.role
261 && why.trim().is_empty()
262 {
263 return Err(at(format!(
264 "output {}{} is Unknown with no why",
265 o.label.name, o.label.index
266 )));
267 }
268 if let Some(before) = self.outputs[..n]
270 .iter()
271 .find(|before| before.label == o.label && before.lanes & o.lanes != 0)
272 {
273 return Err(at(format!(
274 "output {}{} has two roles on lanes {:#x}",
275 o.label.name,
276 o.label.index,
277 before.lanes & o.lanes
278 )));
279 }
280 }
281 if self.stage == Stage::Pixel
284 && let Some((label, lanes)) = self.unbound_written_lanes(interface).first()
285 {
286 return Err(at(format!(
287 "output {}{} lanes {lanes:#06b} are written and have no binding",
288 label.name, label.index
289 )));
290 }
291 for c in &self.constants {
292 let Some(ResourceKind::ConstantBuffer { layout }) =
293 interface.resource(c.resource).map(|r| &r.kind)
294 else {
295 return Err(at(format!(
296 "constant resource {} is not a constant buffer",
297 c.resource.0
298 )));
299 };
300 if c.size == 0 || c.offset.checked_add(c.size).is_none_or(|end| end > layout.size) {
301 return Err(at(format!(
302 "constant of {} bytes at {} is outside buffer {} of {} bytes",
303 c.size, c.offset, c.resource.0, layout.size
304 )));
305 }
306 let named = c.name.as_deref().map_or(String::new(), |n| format!("{n} "));
307 match (c.role.is_matrix(), &c.lines) {
308 (true, None) => {
309 return Err(at(format!(
310 "constant {named}at {} is a matrix ({:?}) whose lines are not stated",
311 c.offset, c.role
312 )));
313 }
314 (true, Some(MatrixLines::Unshown(Unshown::Unexamined { why }))) if why.trim().is_empty() => {
315 return Err(at(format!(
316 "constant {named}at {} is a matrix whose lines are unshown with no reason",
317 c.offset
318 )));
319 }
320 (false, Some(lines)) => {
321 return Err(at(format!(
322 "constant {named}at {} states lines ({lines:?}) for a role that is no matrix ({:?})",
323 c.offset, c.role
324 )));
325 }
326 _ => {}
327 }
328 if let ConstantRole::AlphaReference { value: r, .. } = c.role
329 && (!r.is_finite() || c.size != 4)
330 {
331 return Err(at(format!(
332 "the alpha reference {r} at {} is not one finite float ({} bytes)",
333 c.offset, c.size
334 )));
335 }
336 if let ConstantRole::Material(words) = &c.role
337 && words.len() as u64 * 4 > u64::from(c.size)
338 {
339 return Err(at(format!(
340 "{} material words for a constant of {} bytes at {}",
341 words.len(),
342 c.size,
343 c.offset
344 )));
345 }
346 if let ConstantRole::Default { words, why } = &c.role {
347 if words.len() as u64 * 4 > u64::from(c.size) {
348 return Err(at(format!(
349 "{} default words for a constant of {} bytes at {}",
350 words.len(),
351 c.size,
352 c.offset
353 )));
354 }
355 if why.trim().is_empty() {
356 return Err(at(format!(
357 "the default of the constant at {} says no why",
358 c.offset
359 )));
360 }
361 }
362 }
363 let mut spans: Vec<(u32, u32, u32)> = self
365 .constants
366 .iter()
367 .map(|c| (c.resource.0, c.offset, c.offset + c.size))
368 .collect();
369 spans.sort_unstable();
370 if let Some(pair) = spans.windows(2).find(|p| p[0].0 == p[1].0 && p[1].1 < p[0].2) {
371 return Err(at(format!(
372 "constants overlap in buffer {}: bytes {}..{} and {}..{}",
373 pair[0].0, pair[0].1, pair[0].2, pair[1].1, pair[1].2
374 )));
375 }
376 for (n, t) in self.textures.iter().enumerate() {
377 if let TextureSource::Missing { why } = &t.source
378 && why.trim().is_empty()
379 {
380 return Err(at(format!(
381 "texture slot {} is missing without a reason",
382 t.resource.0
383 )));
384 }
385 let Some(ResourceKind::Texture { dim, arrayed, .. }) =
386 interface.resource(t.resource).map(|r| &r.kind)
387 else {
388 return Err(at(format!("resource {} is not a texture", t.resource.0)));
389 };
390 if self.textures[..n]
391 .iter()
392 .any(|before| before.resource == t.resource)
393 {
394 return Err(at(format!("texture slot {} is bound twice", t.resource.0)));
395 }
396 if place == BindingPlace::Material && matches!(t.source, TextureSource::Target { .. }) {
397 return Err(at(format!(
398 "texture slot {} binds a render graph target, which only a step of a core.render_graph may",
399 t.resource.0
400 )));
401 }
402 if let TextureSource::Frame(frame) = &t.source
403 && *dim != frame.dim()
404 {
405 return Err(at(format!(
406 "texture slot {} is a {dim:?} texture and binds the frame's {frame:?}, a {:?} image",
407 t.resource.0,
408 frame.dim()
409 )));
410 }
411 if let Some(s) = t.sampler
412 && !matches!(
413 interface.resource(s).map(|r| &r.kind),
414 Some(ResourceKind::Sampler { .. })
415 )
416 {
417 return Err(at(format!("resource {} is not a sampler", s.0)));
418 }
419 if let TextureSource::Texture { texture, .. } = &t.source {
420 if !(w.has::<Texture>(*texture)
421 || w.has::<TextureLayers>(*texture)
422 || w.has::<TextureFile>(*texture))
423 {
424 return Err(at(format!(
425 "texture slot {} binds an entity that is neither core.texture, core.texture_layers nor core.texture_file",
426 t.resource.0
427 )));
428 }
429 if let Some(layers) = w.get::<TextureLayers>(*texture)
431 && layers.shape.dimension() != (*dim, *arrayed)
432 {
433 return Err(at(format!(
434 "texture slot {} is a {dim:?} resource (arrayed {arrayed}) and binds a {:?} texture",
435 t.resource.0, layers.shape
436 )));
437 }
438 }
439 }
440 self.verify_samplers(interface, place).map_err(at)?;
441 self.verify_buffers(w, interface, place).map_err(at)?;
442 self.verify_constant_buffers(interface).map_err(at)
443 }
444
445 fn verify_samplers(&self, interface: &Interface, place: BindingPlace) -> Result<(), String> {
448 if place == BindingPlace::Material
449 && let Some(first) = self.samplers.first()
450 {
451 return Err(format!(
452 "sampler {} states its own state, which only a step of a core.render_graph may (a material's sampler states are its textures')",
453 first.resource.0
454 ));
455 }
456 for (n, s) in self.samplers.iter().enumerate() {
457 if !matches!(
458 interface.resource(s.resource).map(|r| &r.kind),
459 Some(ResourceKind::Sampler { .. })
460 ) {
461 return Err(format!("resource {} is not a sampler", s.resource.0));
462 }
463 if self.samplers[..n]
464 .iter()
465 .any(|before| before.resource == s.resource)
466 {
467 return Err(format!("sampler {} is stated twice", s.resource.0));
468 }
469 s.state
470 .verify()
471 .map_err(|why| format!("sampler {}: {why}", s.resource.0))?;
472 }
473 Ok(())
474 }
475
476 pub fn buffer(&self, resource: ResourceId) -> BufferSource {
479 self.buffers
480 .iter()
481 .find(|b| b.resource == resource)
482 .map(|b| b.source.clone())
483 .unwrap_or_else(|| BufferSource::Unknown {
484 why: "the source states no buffers for this stage".into(),
485 })
486 }
487
488 fn verify_constant_buffers(&self, interface: &Interface) -> Result<(), String> {
491 for (n, c) in self.constant_buffers.iter().enumerate() {
492 let Some(ResourceKind::ConstantBuffer { layout }) =
493 interface.resource(c.resource).map(|r| &r.kind)
494 else {
495 return Err(format!("resource {} is not a constant buffer", c.resource.0));
496 };
497 if self.constant_buffers[..n]
498 .iter()
499 .any(|before| before.resource == c.resource)
500 {
501 return Err(format!("constant buffer {} is bound twice", c.resource.0));
502 }
503 if let Some(bytes) = c.bound_size
504 && (bytes % 16 != 0 || bytes < layout.size)
505 {
506 return Err(format!(
507 "constant buffer {} is bound at {bytes} bytes: not a whole number of 16-byte rows, or smaller than the {} the program declares",
508 c.resource.0, layout.size
509 ));
510 }
511 }
512 Ok(())
513 }
514
515 pub fn bound_size(&self, resource: ResourceId) -> Option<u32> {
518 self.constant_buffers
519 .iter()
520 .find(|c| c.resource == resource)
521 .and_then(|c| c.bound_size)
522 }
523
524 pub fn bone_palette_matrices(&self) -> Option<u32> {
530 let c = self.constants.iter().find(|c| {
531 matches!(c.role, ConstantRole::Object(ObjectParameter::BonePalette))
532 && matches!(c.lines, Some(MatrixLines::Rows))
533 })?;
534 let bound = self
535 .bound_size(c.resource)
536 .map_or(0, |b| b.saturating_sub(c.offset));
537 Some(c.size.max(bound) / 48)
538 }
539
540 fn verify_buffers(
547 &self,
548 w: &crate::World,
549 interface: &Interface,
550 place: BindingPlace,
551 ) -> Result<(), String> {
552 let kind = |id: ResourceId| match interface.resource(id).map(|r| &r.kind) {
553 Some(ResourceKind::Buffer(kind)) => Some(*kind),
554 _ => None,
555 };
556 let lists: Vec<&super::LightListPart> = self
557 .buffers
558 .iter()
559 .filter_map(|b| match &b.source {
560 BufferSource::LightLists(part) => Some(part),
561 _ => None,
562 })
563 .collect();
564 if !lists.is_empty() {
565 let lookups: Vec<_> = lists
566 .iter()
567 .filter_map(|p| match p {
568 super::LightListPart::Lookup(l) => Some(l),
569 super::LightListPart::Indices(_) => None,
570 })
571 .collect();
572 let [lookup] = lookups.as_slice() else {
573 return Err(format!(
574 "light lists with {} lookups: a stage's lists are one lookup and one index list",
575 lookups.len()
576 ));
577 };
578 if lists.len() != 2 {
579 return Err(format!(
580 "{} light list buffers: a stage's lists are one lookup and one index list",
581 lists.len()
582 ));
583 }
584 for (counted, _) in &lookup.counts {
585 if !self
586 .buffers
587 .iter()
588 .any(|b| matches!(&b.source, BufferSource::Lights(t) if t.kind == *counted))
589 {
590 return Err(format!(
591 "light lists count {counted:?} lights and the stage binds no {counted:?} light table"
592 ));
593 }
594 }
595 }
596 for (n, b) in self.buffers.iter().enumerate() {
597 let Some(buffer) = kind(b.resource) else {
598 return Err(format!("resource {} is not a buffer", b.resource.0));
599 };
600 if self.buffers[..n]
601 .iter()
602 .any(|before| before.resource == b.resource)
603 {
604 return Err(format!("buffer slot {} is bound twice", b.resource.0));
605 }
606 match &b.source {
607 BufferSource::Unknown { why } if why.is_empty() => {
608 return Err(format!(
609 "buffer slot {} is unknown and says not why",
610 b.resource.0
611 ));
612 }
613 BufferSource::Unknown { .. } => {}
614 BufferSource::Graph(_) if place == BindingPlace::Material => {
615 return Err(format!(
616 "buffer slot {} binds a render graph buffer, which only a step of a core.render_graph may",
617 b.resource.0
618 ));
619 }
620 BufferSource::Graph(_) => {}
621 BufferSource::LightLists(part) => {
622 let stride = match buffer {
623 BufferKind::Structured { stride } => Some(stride),
624 _ => None,
625 };
626 part.verify(stride)
627 .map_err(|why| format!("buffer slot {}: {why}", b.resource.0))?;
628 }
629 BufferSource::Lights(table) => {
630 let stride = match buffer {
631 BufferKind::Structured { stride } => Some(stride),
632 _ => None,
633 };
634 table
635 .verify(stride)
636 .map_err(|why| format!("buffer slot {}: {why}", b.resource.0))?;
637 if w.resource::<crate::LightUnit>().is_none()
638 && w.iter::<crate::components::Light>().any(|(_, l)| table.holds(l))
639 {
640 return Err(format!(
641 "buffer slot {} is a light table in a world whose lights have no core.light_unit: its \
642 colours have no value",
643 b.resource.0
644 ));
645 }
646 }
647 BufferSource::Table(
648 BufferTable::ObjectMatrices { rows }
649 | BufferTable::HeaderedMatrices { rows }
650 | BufferTable::SkinnedMatrices { rows, .. },
651 ) => {
652 if !(3..=4).contains(rows) {
653 return Err(format!(
654 "buffer slot {} holds matrices of {rows} rows, not 3 or 4",
655 b.resource.0
656 ));
657 }
658 if let BufferKind::Structured { stride } = buffer
659 && stride != 16
660 {
661 return Err(format!(
662 "buffer slot {} holds matrix rows of 16 bytes and has elements of {stride}",
663 b.resource.0
664 ));
665 }
666 if let BufferSource::Table(BufferTable::SkinnedMatrices {
667 joint_stride, prefix, ..
668 }) = &b.source
669 {
670 if joint_stride < rows {
671 return Err(format!(
672 "buffer slot {} holds joints {joint_stride} elements apart with {rows} rows each",
673 b.resource.0
674 ));
675 }
676 if *prefix == 0 {
677 return Err(format!(
678 "buffer slot {} holds skinned matrices with no prefix element to say what the first one holds",
679 b.resource.0
680 ));
681 }
682 }
683 }
684 BufferSource::Table(BufferTable::Instances {
685 stride,
686 matrix_start,
687 matrices,
688 }) => {
689 if *stride == 0 || matrix_start >= stride {
690 return Err(format!(
691 "buffer slot {} holds instance records of {stride} words and names word {matrix_start}",
692 b.resource.0
693 ));
694 }
695 if let BufferKind::Structured { stride: bytes } = buffer
696 && bytes != u32::from(*stride) * 4
697 {
698 return Err(format!(
699 "buffer slot {} holds instance records of {stride} words and has elements of {bytes} bytes",
700 b.resource.0
701 ));
702 }
703 let is_matrices = |other: &BufferBinding| {
704 other.resource == *matrices
705 && matches!(
706 other.source,
707 BufferSource::Table(
708 BufferTable::ObjectMatrices { .. }
709 | BufferTable::HeaderedMatrices { .. }
710 | BufferTable::SkinnedMatrices { .. }
711 )
712 )
713 };
714 if !self.buffers.iter().any(is_matrices) {
715 return Err(format!(
716 "buffer slot {} names slot {} for its matrices, which is no matrix table of this stage",
717 b.resource.0, matrices.0
718 ));
719 }
720 }
721 }
722 }
723 if !self.buffers.is_empty()
724 && let Some(r) = interface.resources.iter().find(|r| {
725 matches!(r.kind, ResourceKind::Buffer(_)) && !self.buffers.iter().any(|b| b.resource == r.id)
726 })
727 {
728 return Err(format!(
729 "buffer slot {} has no entry: a stage that states buffers states every one (a table, or unknown with \
730 why)",
731 r.id.0
732 ));
733 }
734 Ok(())
735 }
736}
737impl Component for ShaderBinding {
738 const NAME: &'static str = "core.shader_binding";
739 const GEOMETRY: crate::Geometry = crate::Geometry::None;
740 fn to_json(&self) -> Result<Option<Value>, JsonError> {
741 crate::json::to_value(self).map(Some)
742 }
743 fn verify(&self, _e: Entity, w: &crate::World) -> Result<(), String> {
744 for (i, s) in self.stages.iter().enumerate() {
745 if let Some(k) = &s.variant {
746 if k.field.is_empty() {
747 return Err(format!("the {:?} stage's variant key names no field", s.stage));
748 }
749 if let Some(p) = k.parts.iter().enumerate().find_map(|(n, p)| {
750 k.parts[..n]
751 .iter()
752 .any(|before| before.name == p.name)
753 .then_some(&p.name)
754 }) {
755 return Err(format!(
756 "the variant key {} = {} describes {p:?} twice",
757 k.field, k.value
758 ));
759 }
760 if let Some(other) = self.stages[..i].iter().find(|o| {
762 o.variant
763 .as_ref()
764 .is_some_and(|b| b.field == k.field && b.value == k.value && b.parts != k.parts)
765 }) {
766 return Err(format!(
767 "the {:?} and {:?} stages of {} = {} describe it differently",
768 other.stage, s.stage, k.field, k.value
769 ));
770 }
771 }
772 for other in self.stages[..i].iter().filter(|o| o.stage == s.stage) {
774 let distinct = match (&other.variant, &s.variant) {
775 (Some(a), Some(b)) => a.field == b.field && a.value != b.value,
776 _ => false,
777 };
778 if !distinct {
779 return Err(match &s.variant {
780 None => format!("the {:?} stage is bound twice", s.stage),
781 Some(k) => format!(
782 "the {:?} stage is bound twice for {} = {} (several need distinct values of one key)",
783 s.stage, k.field, k.value
784 ),
785 });
786 }
787 }
788 s.verify_in(w, BindingPlace::Material)?;
789 }
790 self.state.verify()
791 }
792 fn references(&self, out: &mut Vec<Entity>) {
793 for s in &self.stages {
794 out.push(s.shader);
795 for t in &s.textures {
796 if let TextureSource::Texture { texture, .. } = &t.source {
797 out.push(*texture);
798 }
799 }
800 }
801 }
802 fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
803 crate::world::Linearity::Invariant
804 }
805}
806
807#[cfg(test)]
808mod tests {
809 use super::*;
810 use crate::World;
811 use crate::shader::Attribute;
812 use crate::shader::{InputRole, KeyPart, Occluded, ScalarEncoding, SkinPrefix, ViewParameter};
813 use iridia_interface::AddressMode;
814 use iridia_interface::{
815 BufferLayout, Constant, ConstantType, ResourceProvenance, SampleType, ScalarType, ShaderFormat,
816 TextureDim, Value as Lanes, Varying, VaryingProvenance,
817 };
818 use iridia_interface::{Resource, ResourceKind as Kind};
819 fn varying(name: &str, width: u8) -> Varying {
820 Varying {
821 label: Label {
822 name: name.into(),
823 index: 0,
824 },
825 system: None,
826 value: Lanes {
827 scalar: ScalarType::Float,
828 width,
829 },
830 interp: None,
831 used: true,
832 provenance: VaryingProvenance::default(),
833 }
834 }
835 fn resource(id: u32, kind: Kind) -> Resource {
836 Resource {
837 id: ResourceId(id),
838 kind,
839 count: 1,
840 provenance: ResourceProvenance::default(),
841 }
842 }
843 fn shader() -> Shader {
845 Shader {
846 format: ShaderFormat::Dxbc,
847 bytecode: vec![0; 8],
848 interface: Interface {
849 stage: Stage::Pixel,
850 inputs: vec![varying("TEXCOORD", 2)],
851 outputs: vec![varying("SV_Target", 4)],
852 resources: vec![
853 resource(
854 0,
855 Kind::ConstantBuffer {
856 layout: BufferLayout {
857 size: 32,
858 constants: vec![Constant {
859 name: "tint".into(),
860 offset: 0,
861 ty: ConstantType::Vector(Lanes {
862 scalar: ScalarType::Float,
863 width: 4,
864 }),
865 default: None,
866 }],
867 },
868 },
869 ),
870 resource(
871 1,
872 Kind::Texture {
873 dim: TextureDim::D2,
874 arrayed: false,
875 multisampled: false,
876 sample: SampleType::Float,
877 },
878 ),
879 resource(2, Kind::Sampler { comparison: false }),
880 ],
881 facts: Default::default(),
882 },
883 }
884 }
885 fn binding(shader: Entity, texture: Entity) -> ShaderBinding {
886 ShaderBinding {
887 stages: vec![StageBinding {
888 inputs: vec![InputBinding {
889 label: Label {
890 name: "TEXCOORD".into(),
891 index: 0,
892 },
893 role: InputRole::Stream(Attribute::TexCoord(0)),
894 }],
895 constants: vec![
896 ConstantBinding {
897 resource: ResourceId(0),
898 offset: 0,
899 size: 16,
900 name: Some("tint".into()),
901 role: ConstantRole::Material(vec![1.0f32.to_bits(); 4]),
902 lines: None,
903 },
904 ConstantBinding {
905 resource: ResourceId(0),
906 offset: 16,
907 size: 16,
908 name: None,
909 role: ConstantRole::Unknown {
910 why: "cb0[1] is not in the recipe".into(),
911 },
912 lines: None,
913 },
914 ],
915 textures: vec![TextureBinding {
916 resource: ResourceId(1),
917 sampler: Some(ResourceId(2)),
918 source: TextureSource::Texture {
919 texture,
920 sampler: SamplerState {
921 wrap: [Some(AddressMode::Repeat); 3],
922 ..SamplerState::default()
923 },
924 },
925 }],
926 outputs: vec![
927 OutputBinding {
928 label: Label {
929 name: "SV_Target".into(),
930 index: 0,
931 },
932 lanes: 0b0111,
933 role: OutputRole::Albedo,
934 },
935 OutputBinding {
936 label: Label {
937 name: "SV_Target".into(),
938 index: 0,
939 },
940 lanes: 0b1000,
941 role: OutputRole::Occlusion {
942 of: Occluded::AllLight,
943 encoding: ScalarEncoding::LINEAR,
944 },
945 },
946 ],
947 ..StageBinding::new(Stage::Pixel, shader)
948 }],
949 state: RenderState::default(),
950 }
951 }
952 fn buffer_shader() -> Shader {
954 let structured = |id, stride| {
955 resource(
956 id,
957 Kind::Buffer(iridia_interface::BufferKind::Structured { stride }),
958 )
959 };
960 Shader {
961 format: ShaderFormat::Dxbc,
962 bytecode: vec![0; 8],
963 interface: Interface {
964 stage: Stage::Vertex,
965 inputs: Vec::new(),
966 outputs: Vec::new(),
967 resources: vec![
968 resource(
969 0,
970 Kind::ConstantBuffer {
971 layout: BufferLayout {
972 size: 16,
973 constants: Vec::new(),
974 },
975 },
976 ),
977 structured(10, 16),
978 structured(11, 8),
979 structured(12, 12),
980 ],
981 facts: Default::default(),
982 },
983 }
984 }
985 fn buffer_binding(shader: Entity, buffers: Vec<BufferBinding>) -> ShaderBinding {
986 ShaderBinding {
987 stages: vec![StageBinding {
988 buffers,
989 ..StageBinding::new(Stage::Vertex, shader)
990 }],
991 state: RenderState::default(),
992 }
993 }
994 fn matrices(resource: u32, rows: u8) -> BufferBinding {
995 BufferBinding {
996 resource: ResourceId(resource),
997 source: BufferSource::Table(BufferTable::ObjectMatrices { rows }),
998 }
999 }
1000 fn headered(resource: u32, rows: u8) -> BufferBinding {
1001 BufferBinding {
1002 resource: ResourceId(resource),
1003 source: BufferSource::Table(BufferTable::HeaderedMatrices { rows }),
1004 }
1005 }
1006 fn skinned(resource: u32, rows: u8, joint_stride: u8, prefix: u8) -> BufferBinding {
1007 BufferBinding {
1008 resource: ResourceId(resource),
1009 source: BufferSource::Table(BufferTable::SkinnedMatrices {
1010 rows,
1011 joint_stride,
1012 prefix,
1013 first: SkinPrefix::Translation,
1014 }),
1015 }
1016 }
1017 fn instances(resource: u32, stride: u8, matrix_start: u8, table: u32) -> BufferBinding {
1018 BufferBinding {
1019 resource: ResourceId(resource),
1020 source: BufferSource::Table(BufferTable::Instances {
1021 stride,
1022 matrix_start,
1023 matrices: ResourceId(table),
1024 }),
1025 }
1026 }
1027 #[test]
1030 fn a_material_binds_no_render_graph_buffer() {
1031 let mut w = World::new();
1032 let (s, m) = (w.spawn(), w.spawn());
1033 w.insert(s, buffer_shader());
1034 let graph = |resource: u32| BufferBinding {
1035 resource: ResourceId(resource),
1036 source: BufferSource::Graph(0),
1037 };
1038 let b = buffer_binding(s, vec![graph(10), graph(11), graph(12)]);
1039 w.insert(m, b.clone());
1040 let err = w.get::<ShaderBinding>(m).unwrap().verify(m, &w).unwrap_err();
1041 assert!(
1042 err.contains(
1043 "buffer slot 10 binds a render graph buffer, which only a step of a core.render_graph may"
1044 ),
1045 "{err}"
1046 );
1047 assert_eq!(b.stages[0].verify_in(&w, BindingPlace::GraphStep), Ok(()));
1048 }
1049 #[test]
1052 fn buffer_slots_name_the_tables_the_program_indexes() {
1053 let mut w = World::new();
1054 let (s, m) = (w.spawn(), w.spawn());
1055 w.insert(s, buffer_shader());
1056 let rest = || BufferBinding {
1058 resource: ResourceId(12),
1059 source: BufferSource::Unknown {
1060 why: "not read here".into(),
1061 },
1062 };
1063 let good = vec![matrices(10, 3), instances(11, 2, 0, 10), rest()];
1064 w.insert(m, buffer_binding(s, good.clone()));
1065 assert_eq!(w.get::<ShaderBinding>(m).unwrap().verify(m, &w), Ok(()));
1066 let json = w.get::<ShaderBinding>(m).unwrap().to_json().unwrap().unwrap();
1067 assert_eq!(
1068 json["stages"][0]["buffers"][0]["source"],
1069 serde_json::json!({"table": {"object_matrices": {"rows": 3}}})
1070 );
1071 assert_eq!(
1072 json["stages"][0]["buffers"][1]["source"],
1073 serde_json::json!({"table": {"instances": {"stride": 2, "matrix_start": 0, "matrices": 10}}})
1074 );
1075 let unknown = |why: &str| BufferBinding {
1076 resource: ResourceId(12),
1077 source: BufferSource::Unknown { why: why.into() },
1078 };
1079 let cases: Vec<(Vec<BufferBinding>, &str)> = vec![
1080 (vec![matrices(0, 3)], "is not a buffer"),
1081 (vec![matrices(99, 3)], "is not a buffer"),
1082 (vec![matrices(10, 3), matrices(10, 4)], "bound twice"),
1083 (vec![matrices(10, 5)], "not 3 or 4"),
1084 (vec![matrices(11, 3)], "has elements of 8"),
1085 (vec![matrices(10, 3), instances(11, 2, 2, 10)], "names word 2"),
1086 (vec![matrices(10, 3), instances(11, 0, 0, 10)], "names word 0"),
1087 (
1088 vec![matrices(10, 3), instances(11, 3, 0, 10)],
1089 "has elements of 8 bytes",
1090 ),
1091 (
1092 vec![matrices(10, 3), instances(11, 2, 0, 12)],
1093 "no matrix table of this stage",
1094 ),
1095 (vec![instances(11, 2, 0, 10)], "no matrix table of this stage"),
1096 (
1097 vec![matrices(10, 3), instances(11, 2, 0, 11)],
1098 "no matrix table of this stage",
1099 ),
1100 (vec![unknown("")], "says not why"),
1101 ];
1102 for (buffers, message) in cases {
1103 w.insert(m, buffer_binding(s, buffers.clone()));
1104 let err = w.get::<ShaderBinding>(m).unwrap().verify(m, &w).unwrap_err();
1105 assert!(err.contains(message), "{buffers:?}: {err}");
1106 }
1107 w.insert(
1109 m,
1110 buffer_binding(
1111 s,
1112 vec![
1113 matrices(10, 4),
1114 instances(11, 2, 0, 10),
1115 unknown("the engine fills it"),
1116 ],
1117 ),
1118 );
1119 assert_eq!(w.get::<ShaderBinding>(m).unwrap().verify(m, &w), Ok(()));
1120 w.insert(
1122 m,
1123 buffer_binding(s, vec![headered(10, 3), instances(11, 2, 1, 10), rest()]),
1124 );
1125 assert_eq!(w.get::<ShaderBinding>(m).unwrap().verify(m, &w), Ok(()));
1126 let json = w.get::<ShaderBinding>(m).unwrap().to_json().unwrap().unwrap();
1127 assert_eq!(
1128 json["stages"][0]["buffers"][0]["source"],
1129 serde_json::json!({"table": {"headered_matrices": {"rows": 3}}})
1130 );
1131 w.insert(
1133 m,
1134 buffer_binding(s, vec![skinned(10, 3, 3, 3), instances(11, 2, 1, 10), rest()]),
1135 );
1136 assert_eq!(w.get::<ShaderBinding>(m).unwrap().verify(m, &w), Ok(()));
1137 let json = w.get::<ShaderBinding>(m).unwrap().to_json().unwrap().unwrap();
1138 assert_eq!(
1139 json["stages"][0]["buffers"][0]["source"],
1140 serde_json::json!({"table": {"skinned_matrices": {
1141 "rows": 3, "joint_stride": 3, "prefix": 3, "first": "translation"
1142 }}})
1143 );
1144 for (buffers, message) in [
1145 (
1146 vec![skinned(10, 3, 2, 3), instances(11, 2, 1, 10), rest()],
1147 "elements apart with 3 rows",
1148 ),
1149 (
1150 vec![skinned(10, 3, 3, 0), instances(11, 2, 1, 10), rest()],
1151 "no prefix element to say what the first one holds",
1152 ),
1153 (
1154 vec![skinned(10, 5, 5, 3), instances(11, 2, 1, 10), rest()],
1155 "not 3 or 4",
1156 ),
1157 ] {
1158 w.insert(m, buffer_binding(s, buffers.clone()));
1159 let err = w.get::<ShaderBinding>(m).unwrap().verify(m, &w).unwrap_err();
1160 assert!(err.contains(message), "{buffers:?}: {err}");
1161 assert!(!err.contains(" "), "{err}");
1163 }
1164 w.insert(
1166 m,
1167 buffer_binding(s, vec![matrices(10, 3), instances(11, 2, 0, 10)]),
1168 );
1169 let err = w.get::<ShaderBinding>(m).unwrap().verify(m, &w).unwrap_err();
1170 assert!(err.contains("buffer slot 12 has no entry"), "{err}");
1171 w.insert(m, buffer_binding(s, Vec::new()));
1172 let binding = w.get::<ShaderBinding>(m).unwrap();
1173 assert_eq!(binding.verify(m, &w), Ok(()));
1174 assert_eq!(
1175 binding.stages[0].buffer(ResourceId(10)),
1176 BufferSource::Unknown {
1177 why: "the source states no buffers for this stage".into()
1178 }
1179 );
1180 w.insert(m, buffer_binding(s, good.clone()));
1181 assert_eq!(
1182 w.get::<ShaderBinding>(m).unwrap().stages[0].buffer(ResourceId(10)),
1183 BufferSource::Table(BufferTable::ObjectMatrices { rows: 3 })
1184 );
1185 for (buffers, message) in [
1186 (vec![headered(10, 5)], "not 3 or 4"),
1187 (vec![headered(11, 3)], "has elements of 8"),
1188 (vec![headered(10, 3), matrices(10, 3)], "bound twice"),
1189 (vec![headered(0, 3)], "is not a buffer"),
1190 ] {
1191 w.insert(m, buffer_binding(s, buffers.clone()));
1192 let err = w.get::<ShaderBinding>(m).unwrap().verify(m, &w).unwrap_err();
1193 assert!(err.contains(message), "{buffers:?}: {err}");
1194 }
1195 }
1196 #[test]
1199 fn a_constant_buffer_is_bound_at_a_whole_number_of_rows_no_smaller_than_declared() {
1200 let mut w = World::new();
1201 let (s, m) = (w.spawn(), w.spawn());
1202 w.insert(s, buffer_shader());
1203 let bound = |resource: u32, bound_size| ConstantBufferBinding {
1204 resource: ResourceId(resource),
1205 bound_size,
1206 };
1207 let with = |constant_buffers: Vec<ConstantBufferBinding>| {
1208 let mut b = buffer_binding(s, Vec::new());
1209 b.stages[0].constant_buffers = constant_buffers;
1210 b
1211 };
1212 w.insert(m, with(vec![bound(0, Some(12_288))]));
1213 let binding = w.get::<ShaderBinding>(m).unwrap();
1214 assert_eq!(binding.verify(m, &w), Ok(()));
1215 assert_eq!(binding.stages[0].bound_size(ResourceId(0)), Some(12_288));
1216 let json = binding.to_json().unwrap().unwrap();
1217 assert_eq!(
1218 json["stages"][0]["constant_buffers"][0],
1219 serde_json::json!({"resource": 0, "bound_size": 12288})
1220 );
1221 let palette = |lines| ConstantBinding {
1224 resource: ResourceId(0),
1225 offset: 0,
1226 size: 96,
1227 name: None,
1228 role: ConstantRole::Object(ObjectParameter::BonePalette),
1229 lines,
1230 };
1231 let mut stage = binding.stages[0].clone();
1232 stage.constants = vec![palette(Some(MatrixLines::Rows))];
1233 assert_eq!(stage.bone_palette_matrices(), Some(256));
1234 stage.constant_buffers.clear();
1235 assert_eq!(stage.bone_palette_matrices(), Some(2));
1236 stage.constants = vec![palette(Some(MatrixLines::Columns))];
1237 assert_eq!(stage.bone_palette_matrices(), None);
1238 stage.constants.clear();
1239 assert_eq!(stage.bone_palette_matrices(), None);
1240 w.insert(m, with(vec![bound(0, None)]));
1242 let binding = w.get::<ShaderBinding>(m).unwrap();
1243 assert_eq!(binding.verify(m, &w), Ok(()));
1244 assert_eq!(binding.stages[0].bound_size(ResourceId(0)), None);
1245 assert_eq!(binding.stages[0].bound_size(ResourceId(10)), None);
1246 for (buffers, message) in [
1247 (vec![bound(10, Some(64))], "is not a constant buffer"),
1248 (vec![bound(99, Some(64))], "is not a constant buffer"),
1249 (vec![bound(0, Some(32)), bound(0, None)], "bound twice"),
1250 (vec![bound(0, Some(40))], "not a whole number of 16-byte rows"),
1251 (
1252 vec![bound(0, Some(0))],
1253 "smaller than the 16 the program declares",
1254 ),
1255 ] {
1256 w.insert(m, with(buffers.clone()));
1257 let err = w.get::<ShaderBinding>(m).unwrap().verify(m, &w).unwrap_err();
1258 assert!(err.contains(message), "{buffers:?}: {err}");
1259 }
1260 }
1261 fn world() -> (World, Entity, Entity, Entity) {
1263 let mut w = World::new();
1264 let (s, t, m) = (w.spawn(), w.spawn(), w.spawn());
1265 w.insert(s, shader());
1266 w.insert(
1267 t,
1268 TextureFile {
1269 path: "albedo.png".into(),
1270 },
1271 );
1272 w.insert(m, binding(s, t));
1273 (w, s, t, m)
1274 }
1275 #[test]
1276 fn an_unfilled_texture_retains_its_reason_and_cannot_omit_it() {
1277 let (w, _, _, material) = world();
1278 let mut binding = w.get::<ShaderBinding>(material).unwrap().clone();
1279 let reason = "engine texture t3 space 0: the runtime image is not stored in the material";
1280 binding.stages[0].textures[0].source = TextureSource::Missing { why: reason.into() };
1281 assert_eq!(binding.verify(material, &w), Ok(()));
1282 let json = serde_json::to_value(&binding).unwrap();
1283 assert_eq!(
1284 json["stages"][0]["textures"][0]["source"]["missing"]["why"],
1285 reason
1286 );
1287 for why in ["", " \n"] {
1288 binding.stages[0].textures[0].source = TextureSource::Missing { why: why.into() };
1289 let error = binding.verify(material, &w).unwrap_err();
1290 assert!(
1291 error.contains("texture slot 1 is missing without a reason"),
1292 "{error}"
1293 );
1294 }
1295 }
1296
1297 #[test]
1299 fn an_encoding_of_no_power_no_root_or_no_finite_scale_is_refused() {
1300 let one = 1f32.to_bits();
1301 for bad in [
1302 ScalarEncoding {
1303 power: 0,
1304 root: 1,
1305 scale_bits: one,
1306 },
1307 ScalarEncoding {
1308 power: 1,
1309 root: 0,
1310 scale_bits: one,
1311 },
1312 ScalarEncoding {
1313 power: 1,
1314 root: 1,
1315 scale_bits: f32::INFINITY.to_bits(),
1316 },
1317 ] {
1318 assert!(bad.check().is_err(), "{bad:?}");
1319 let (mut w, _, _, m) = world();
1320 let mut b = w.get::<ShaderBinding>(m).unwrap().clone();
1321 b.stages[0].outputs[1].role = OutputRole::Occlusion {
1322 of: Occluded::AllLight,
1323 encoding: bad,
1324 };
1325 w.insert(m, b);
1326 let err = w.get::<ShaderBinding>(m).unwrap().verify(m, &w).unwrap_err();
1327 assert!(err.contains("output SV_Target0: an encoding of"), "{err}");
1328 }
1329 assert_eq!(ScalarEncoding::square_of(0.95).check(), Ok(()));
1330 assert_eq!(ScalarEncoding::LINEAR.check(), Ok(()));
1331 }
1332 #[test]
1335 fn a_matrix_role_states_its_lines_and_no_other_role_does() {
1336 let (mut w, _, _, m) = world();
1337 let verify = |w: &mut World, role: ConstantRole, lines: Option<MatrixLines>| {
1338 let mut b = w.get::<ShaderBinding>(m).unwrap().clone();
1339 b.stages[0].constants[0].role = role;
1340 b.stages[0].constants[0].lines = lines;
1341 w.insert(m, b);
1342 w.get::<ShaderBinding>(m).unwrap().verify(m, w)
1343 };
1344 let view_projection = ConstantRole::View(ViewParameter::ViewProjection);
1345 let err = verify(&mut w, view_projection.clone(), None).unwrap_err();
1346 assert!(
1347 err.contains("constant tint at 0 is a matrix") && err.contains("lines are not stated"),
1348 "{err}"
1349 );
1350 let unexamined = |why: &str| MatrixLines::Unshown(Unshown::Unexamined { why: why.into() });
1351 for lines in [
1352 MatrixLines::Rows,
1353 MatrixLines::Columns,
1354 MatrixLines::Unshown(Unshown::NotApplied),
1355 unexamined("no value graph"),
1356 ] {
1357 assert_eq!(verify(&mut w, view_projection.clone(), Some(lines)), Ok(()));
1358 }
1359 let err = verify(&mut w, view_projection.clone(), Some(unexamined(" "))).unwrap_err();
1360 assert!(err.contains("unshown with no reason"), "{err}");
1361 assert_eq!(
1362 verify(
1363 &mut w,
1364 ConstantRole::Object(ObjectParameter::BonePalette),
1365 Some(MatrixLines::Rows)
1366 ),
1367 Ok(())
1368 );
1369 let err = verify(
1370 &mut w,
1371 ConstantRole::Material(vec![0; 4]),
1372 Some(MatrixLines::Rows),
1373 )
1374 .unwrap_err();
1375 assert!(
1376 err.contains("states lines (Rows) for a role that is no matrix"),
1377 "{err}"
1378 );
1379 assert!(!ConstantRole::Object(ObjectParameter::TextureTransformRow(0)).is_matrix());
1380 }
1381 #[test]
1382 fn a_binding_names_what_it_refers_to_and_verifies() {
1383 let (w, s, t, m) = world();
1384 let b = w.get::<ShaderBinding>(m).unwrap();
1385 assert_eq!(b.verify(m, &w), Ok(()));
1386 let mut refs = Vec::new();
1387 b.references(&mut refs);
1388 assert_eq!(refs, [s, t]);
1389 let json = b.to_json().unwrap().unwrap();
1390 assert_eq!(json["stages"][0]["stage"], "pixel");
1391 assert_eq!(
1393 json["stages"][0]["outputs"][1]["role"],
1394 serde_json::json!({
1395 "occlusion": {"of": "all_light", "encoding": {"power": 1, "scale": 1.0}}
1396 })
1397 );
1398 assert_eq!(
1399 json["stages"][0]["constants"][1]["role"]["unknown"]["why"],
1400 "cb0[1] is not in the recipe"
1401 );
1402 assert!(b.stage(Stage::Vertex).is_none() && b.stage(Stage::Pixel).is_some());
1403 }
1404 #[test]
1406 fn each_broken_reference_is_named() {
1407 let (w, _, _, m) = world();
1408 let good = w.get::<ShaderBinding>(m).unwrap().clone();
1409 type Break = Box<dyn Fn(&mut ShaderBinding)>;
1410 let cases: Vec<(Break, &str)> = vec![
1411 (Box::new(|b| b.stages.push(b.stages[0].clone())), "bound twice"),
1412 (
1413 Box::new(|b| b.stages[0].inputs[0].label.index = 7),
1414 "input TEXCOORD7",
1415 ),
1416 (Box::new(|b| b.stages[0].outputs[0].lanes = 0b10000), "lanes"),
1417 (
1418 Box::new(|b| b.stages[0].constants[0].resource = ResourceId(1)),
1419 "not a constant buffer",
1420 ),
1421 (
1422 Box::new(|b| b.stages[0].constants[1].offset = 24),
1423 "outside buffer",
1424 ),
1425 (
1426 Box::new(|b| b.stages[0].constants[0].role = ConstantRole::Material(vec![0; 5])),
1427 "material words",
1428 ),
1429 (
1430 Box::new(|b| {
1431 b.stages[0].constants[0].role = ConstantRole::Default {
1432 words: vec![0; 5],
1433 why: "a policy".into(),
1434 }
1435 }),
1436 "default words",
1437 ),
1438 (
1439 Box::new(|b| {
1440 b.stages[0].constants[0].role = ConstantRole::Default {
1441 words: vec![0],
1442 why: " ".into(),
1443 }
1444 }),
1445 "says no why",
1446 ),
1447 (
1448 Box::new(|b| b.stages[0].textures[0].resource = ResourceId(0)),
1449 "not a texture",
1450 ),
1451 (
1452 Box::new(|b| b.stages[0].textures[0].sampler = Some(ResourceId(1))),
1453 "not a sampler",
1454 ),
1455 (
1456 Box::new(|b| {
1457 let first = b.stages[0].inputs[0].clone();
1458 b.stages[0].inputs.push(first);
1459 }),
1460 "input TEXCOORD0 is bound twice",
1461 ),
1462 (
1463 Box::new(|b| b.stages[0].outputs[1].lanes = 0b1100),
1465 "two roles on lanes 0x4",
1466 ),
1467 (
1468 Box::new(|b| b.stages[0].constants[1].offset = 8),
1469 "constants overlap in buffer 0: bytes 0..16 and 8..24",
1470 ),
1471 (
1472 Box::new(|b| {
1473 let first = b.stages[0].textures[0].clone();
1474 b.stages[0].textures.push(first);
1475 }),
1476 "texture slot 1 is bound twice",
1477 ),
1478 (
1479 Box::new(|b| {
1480 b.stages[0].textures[0].source = TextureSource::Frame(FrameTexture::ReflectionProbe)
1481 }),
1482 "binds the frame's ReflectionProbe, a Cube image",
1483 ),
1484 ];
1485 for (break_it, expect) in cases {
1486 let mut b = good.clone();
1487 break_it(&mut b);
1488 let message = b.verify(m, &w).unwrap_err();
1489 assert!(message.contains(expect), "{expect}: {message}");
1490 }
1491 let mut b = good.clone();
1493 b.stages[0].shader = m;
1494 assert!(b.verify(m, &w).unwrap_err().contains("no core.shader"));
1495 let mut b = good.clone();
1496 b.stages[0].stage = Stage::Vertex;
1497 assert!(b.verify(m, &w).unwrap_err().contains("Pixel program"));
1498 let mut b = good;
1499 if let TextureSource::Texture { texture, .. } = &mut b.stages[0].textures[0].source {
1500 *texture = m;
1501 }
1502 assert!(b.verify(m, &w).unwrap_err().contains("neither core.texture"));
1503 }
1504 #[test]
1508 fn a_pixel_programs_written_lanes_each_have_a_binding() {
1509 let (mut w, s, _, m) = world();
1510 let signature = |w: &mut World, read_write_mask: u8, system: Option<SystemValue>, stage: Stage| {
1511 let mut shader = w.get::<Shader>(s).unwrap().clone();
1512 shader.interface.stage = stage;
1513 let out = &mut shader.interface.outputs[0];
1514 out.system = system;
1515 out.provenance.d3d = Some(iridia_interface::D3dSignature {
1516 register: 0,
1517 mask: 0b1111,
1518 read_write_mask,
1519 });
1520 w.insert(s, shader);
1521 };
1522 let verify = |w: &World, b: &ShaderBinding| b.verify(m, w);
1523 let good = w.get::<ShaderBinding>(m).unwrap().clone();
1524 signature(&mut w, 0, None, Stage::Pixel);
1526 assert_eq!(verify(&w, &good), Ok(()));
1527 let mut rgb = good.clone();
1528 rgb.stages[0].outputs.truncate(1);
1529 assert_eq!(
1530 rgb.stages[0].unbound_written_lanes(&w.get::<Shader>(s).unwrap().interface),
1531 [(&good.stages[0].outputs[0].label, 0b1000)]
1532 );
1533 let err = verify(&w, &rgb).unwrap_err();
1534 assert!(
1535 err.contains("output SV_Target0 lanes 0b1000 are written and have no binding"),
1536 "{err}"
1537 );
1538 signature(&mut w, 0b1000, None, Stage::Pixel);
1540 assert_eq!(verify(&w, &rgb), Ok(()));
1541 signature(&mut w, 0, Some(SystemValue::Target(0)), Stage::Pixel);
1543 assert!(verify(&w, &rgb).is_err());
1544 signature(&mut w, 0, Some(SystemValue::Depth), Stage::Pixel);
1545 assert_eq!(verify(&w, &rgb), Ok(()));
1546 signature(&mut w, 0, None, Stage::Vertex);
1548 let mut vertex = rgb.clone();
1549 vertex.stages[0].stage = Stage::Vertex;
1550 assert_eq!(verify(&w, &vertex), Ok(()));
1551 signature(&mut w, 0, None, Stage::Pixel);
1553 let mut unknown = good;
1554 unknown.stages[0].outputs[1].role = OutputRole::unknown("read by nothing examined");
1555 assert_eq!(verify(&w, &unknown), Ok(()));
1556 unknown.stages[0].outputs[1].role = OutputRole::unknown(" ");
1557 let err = verify(&w, &unknown).unwrap_err();
1558 assert!(err.contains("output SV_Target0 is Unknown with no why"), "{err}");
1559 }
1560 #[test]
1562 fn a_frame_image_fits_the_slot_it_is_bound_to() {
1563 let (w, _, _, m) = world();
1564 let mut b = w.get::<ShaderBinding>(m).unwrap().clone();
1565 b.stages[0].textures[0].source = TextureSource::Frame(FrameTexture::SceneColour);
1566 assert_eq!(b.verify(m, &w), Ok(()));
1567 b.stages[0].textures[0].source = TextureSource::Default {
1568 value: [0.5; 4],
1569 why: "a constant".into(),
1570 };
1571 assert_eq!(b.verify(m, &w), Ok(()));
1572 assert_eq!(FrameTexture::LightProbeVolume.dim(), TextureDim::D3);
1573 assert_eq!(FrameTexture::ShadowMap.dim(), TextureDim::D2);
1574 }
1575 #[test]
1578 fn a_material_binds_no_render_graph_target() {
1579 let (w, _, _, m) = world();
1580 let mut b = w.get::<ShaderBinding>(m).unwrap().clone();
1581 b.stages[0].textures[0].source = TextureSource::Target {
1582 view: crate::render_graph::TargetView {
1583 target: 0,
1584 mip: 0,
1585 first_layer: 0,
1586 layers: 0,
1587 },
1588 };
1589 let err = b.verify(m, &w).unwrap_err();
1590 assert!(
1591 err.contains("binds a render graph target, which only a step of a core.render_graph may"),
1592 "{err}"
1593 );
1594 assert_eq!(b.stages[0].verify_in(&w, BindingPlace::GraphStep), Ok(()));
1595 }
1596 #[test]
1600 fn a_graph_step_states_its_samplers() {
1601 let (w, _, _, m) = world();
1602 let mut b = w.get::<ShaderBinding>(m).unwrap().clone();
1603 let shadow = SamplerState {
1604 wrap: [Some(AddressMode::Border); 3],
1605 compare: Some(iridia_interface::Compare::LessEqual),
1606 border: Some([0.0; 4]),
1607 lod: Some([0.0, 0.0]),
1608 ..SamplerState::default()
1609 };
1610 b.stages[0].samplers = vec![SamplerBinding {
1611 resource: ResourceId(2),
1612 state: shadow,
1613 }];
1614 assert_eq!(b.stages[0].verify_in(&w, BindingPlace::GraphStep), Ok(()));
1615 assert_eq!(b.stages[0].sampler_state(ResourceId(2)), Some(&shadow));
1616 assert_eq!(b.stages[0].sampler_state(ResourceId(1)), None);
1617 let err = b.verify(m, &w).unwrap_err();
1618 assert!(
1619 err.contains("sampler 2 states its own state, which only a step of a core.render_graph may"),
1620 "{err}"
1621 );
1622 let step = |samplers: Vec<SamplerBinding>| {
1623 let mut s = b.stages[0].clone();
1624 s.samplers = samplers;
1625 s.verify_in(&w, BindingPlace::GraphStep)
1626 };
1627 let at = |resource: u32, state: SamplerState| SamplerBinding {
1628 resource: ResourceId(resource),
1629 state,
1630 };
1631 let err = step(vec![at(1, shadow)]).unwrap_err();
1633 assert!(err.contains("resource 1 is not a sampler"), "{err}");
1634 let err = step(vec![at(2, shadow), at(2, SamplerState::default())]).unwrap_err();
1635 assert!(err.contains("sampler 2 is stated twice"), "{err}");
1636 let reversed = SamplerState {
1637 lod: Some([1.0, 0.0]),
1638 ..shadow
1639 };
1640 let err = step(vec![at(2, reversed)]).unwrap_err();
1641 assert!(err.contains("sampler 2: level range"), "{err}");
1642 let json = serde_json::to_value(&b.stages[0].samplers[0]).unwrap();
1644 assert_eq!(json["resource"], serde_json::json!(2));
1645 assert_eq!(json["state"]["border"], serde_json::json!([0.0, 0.0, 0.0, 0.0]));
1646 assert_eq!(json["state"]["compare"], serde_json::json!("less_equal"));
1647 }
1648 #[test]
1650 fn a_layered_texture_fits_the_slot_of_its_shape() {
1651 use crate::{TexFormat, TextureLayers, TextureShape};
1652 let layers = |shape: TextureShape| TextureLayers {
1653 shape,
1654 width: 1,
1655 height: 1,
1656 format: TexFormat::Rgba8Unorm,
1657 levels: vec![(0..shape.layers_at(0)).map(|_| vec![0; 4]).collect()],
1658 color_space: crate::ColorSpace::Linear,
1659 role: crate::TexRole::Other,
1660 normal: None,
1661 };
1662 let (mut w, s, t, m) = world();
1663 let b = w.get::<ShaderBinding>(m).unwrap().clone();
1664 w.remove::<TextureFile>(t);
1665 for shape in [
1667 TextureShape::Array { layers: 2 },
1668 TextureShape::Cube { cubes: 1 },
1669 TextureShape::Volume { depth: 1 },
1670 ] {
1671 w.insert(t, layers(shape));
1672 let why = b.verify(m, &w).unwrap_err();
1673 assert!(
1674 why.contains("is a D2 resource (arrayed false) and binds"),
1675 "{why}"
1676 );
1677 }
1678 let Kind::Texture { arrayed, .. } = &mut w.get_mut::<Shader>(s).unwrap().interface.resources[1].kind
1680 else {
1681 panic!("the fixture's second resource is a texture");
1682 };
1683 *arrayed = true;
1684 w.insert(t, layers(TextureShape::Array { layers: 3 }));
1685 assert_eq!(b.verify(m, &w), Ok(()));
1686 w.insert(t, layers(TextureShape::Cube { cubes: 2 }));
1687 assert!(b.verify(m, &w).is_err());
1688 }
1689 #[test]
1691 fn a_variant_keys_description_is_consistent() {
1692 let (w, _, _, m) = world();
1693 let part = |name: &str, value: u64| KeyPart {
1694 name: name.into(),
1695 value,
1696 };
1697 let key = |field: &str, value: u32, parts: Vec<KeyPart>| {
1698 Some(VariantKey {
1699 field: field.into(),
1700 value,
1701 parts,
1702 })
1703 };
1704 let good = w.get::<ShaderBinding>(m).unwrap().clone();
1705 let with = |a: Option<VariantKey>| {
1706 let mut b = good.clone();
1707 b.stages[0].variant = a;
1708 b
1709 };
1710 assert_eq!(
1711 with(key("f.variant", 0, vec![part("program", 1), part("outputs", 5)])).verify(m, &w),
1712 Ok(())
1713 );
1714 let message = with(key("", 0, Vec::new())).verify(m, &w).unwrap_err();
1715 assert!(message.contains("names no field"), "{message}");
1716 let message = with(key("f.variant", 0, vec![part("program", 1), part("program", 2)]))
1717 .verify(m, &w)
1718 .unwrap_err();
1719 assert!(message.contains("describes \"program\" twice"), "{message}");
1720 let mut b = with(key("f.variant", 3, vec![part("program", 1)]));
1723 let mut other = b.stages[0].clone();
1724 other.stage = Stage::Vertex;
1725 other.variant = key("f.variant", 3, vec![part("program", 2)]);
1726 b.stages.push(other);
1727 let message = b.verify(m, &w).unwrap_err();
1728 assert!(message.contains("describe it differently"), "{message}");
1729 }
1730 #[test]
1733 fn a_stage_repeats_only_under_distinct_values_of_one_key() {
1734 let (w, _, _, m) = world();
1735 let good = w.get::<ShaderBinding>(m).unwrap().clone();
1736 let key = |field: &str, value: u32| {
1737 Some(VariantKey {
1738 field: field.to_owned(),
1739 value,
1740 parts: Vec::new(),
1741 })
1742 };
1743 let with = |a: Option<VariantKey>, b: Option<VariantKey>| {
1744 let mut x = good.clone();
1745 x.stages[0].variant = a;
1746 let mut y = x.stages[0].clone();
1747 y.variant = b;
1748 x.stages.push(y);
1749 x
1750 };
1751 assert_eq!(
1752 with(key("g.mesh.kind", 1), key("g.mesh.kind", 2)).verify(m, &w),
1753 Ok(())
1754 );
1755 let same = with(key("g.mesh.kind", 1), key("g.mesh.kind", 1))
1756 .verify(m, &w)
1757 .unwrap_err();
1758 assert!(same.contains("g.mesh.kind = 1"), "{same}");
1759 assert!(with(key("g.mesh.kind", 1), None).verify(m, &w).is_err());
1760 assert!(with(None, key("g.mesh.kind", 1)).verify(m, &w).is_err());
1761 assert!(
1762 with(None, None)
1763 .verify(m, &w)
1764 .unwrap_err()
1765 .contains("bound twice")
1766 );
1767 assert!(
1768 with(key("g.mesh.kind", 1), key("g.mesh.other", 2))
1769 .verify(m, &w)
1770 .is_err()
1771 );
1772 let mut three = with(key("g.mesh.kind", 1), key("g.mesh.kind", 2));
1775 let mut z = three.stages[0].clone();
1776 z.variant = key("g.mesh.kind", 1);
1777 three.stages.push(z);
1778 assert!(three.verify(m, &w).is_err());
1779 }
1780}