diff --git a/crates/grammar-parser/src/codegen/instructions.rs b/crates/grammar-parser/src/codegen/instructions.rs index 41d3f23c..0616f593 100644 --- a/crates/grammar-parser/src/codegen/instructions.rs +++ b/crates/grammar-parser/src/codegen/instructions.rs @@ -3,6 +3,9 @@ use crate::codegen::{GrammarWriter, OPERAND_ID_RESULT, OPERAND_ID_RESULT_TYPE}; use crate::parse::{Grammar, InstMeta, Operand, Quantifier}; use quote::{format_ident, quote}; +const OP_SWITCH: &str = "OpSwitch"; +const OPERAND_PAIR_LITERAL_INTEGER_ID_REF: &str = "PairLiteralIntegerIdRef"; + pub fn write_inst(writer: &mut GrammarWriter, grammar: &Grammar<'_>) -> anyhow::Result<()> { let insts = grammar.insts.iter().map(|inst| { let struct_ident = InstMeta::type_ident(&inst.opname); @@ -25,10 +28,15 @@ pub fn write_inst(writer: &mut GrammarWriter, grammar: &Grammar<'_>) -> anyhow:: meta, ref name, ref ty, - }| match meta.quantifier { - Quantifier::One => quote!(pub #name: #ty), - Quantifier::ZeroOrOne => quote!(pub #name: ZeroOrOne<#ty>), - Quantifier::ZeroOrMore => quote!(pub #name: ZeroOrMore<#ty>), + }| { + if is_op_switch_targets(inst.opname.as_ref(), meta.kind.as_ref(), meta.quantifier) { + return quote!(pub #name: SwitchTargets); + } + match meta.quantifier { + Quantifier::One => quote!(pub #name: #ty), + Quantifier::ZeroOrOne => quote!(pub #name: ZeroOrOne<#ty>), + Quantifier::ZeroOrMore => quote!(pub #name: ZeroOrMore<#ty>), + } }, ); @@ -157,6 +165,12 @@ pub fn write_inst(writer: &mut GrammarWriter, grammar: &Grammar<'_>) -> anyhow:: ) } +fn is_op_switch_targets(opname: &str, operand_kind: &str, quantifier: Quantifier) -> bool { + opname == OP_SWITCH + && operand_kind == OPERAND_PAIR_LITERAL_INTEGER_ID_REF + && quantifier == Quantifier::ZeroOrMore +} + pub fn write_inst_enum( writer: &mut GrammarWriter, grammar: &Grammar<'_>, diff --git a/crates/grammar-parser/src/lib.rs b/crates/grammar-parser/src/lib.rs index 113f2a19..60e4aa44 100644 --- a/crates/grammar-parser/src/lib.rs +++ b/crates/grammar-parser/src/lib.rs @@ -6,5 +6,7 @@ pub mod timer; #[cfg(feature = "codegen")] pub mod codegen; +#[cfg(feature = "codegen")] pub use proc_macro2; +#[cfg(feature = "codegen")] pub use quote; diff --git a/crates/rspirv2-types/src/binary/decode_error.rs b/crates/rspirv2-types/src/binary/decode_error.rs index 6d4e1298..a66af6f4 100644 --- a/crates/rspirv2-types/src/binary/decode_error.rs +++ b/crates/rspirv2-types/src/binary/decode_error.rs @@ -38,6 +38,9 @@ pub enum DecodeErrorKind { op_len: usize, expected_multiple: usize, }, + InvalidSwitchTargets { + word_len: usize, + }, InstructionTooLong { op_len: usize, module_remaining: usize, @@ -111,6 +114,10 @@ impl Display for DecodeErrorKind { "The variable-sized Instruction has an unexpected operand length {op_len} \ which was expected to be a multiple of {expected_multiple}." ), + Self::InvalidSwitchTargets { word_len } => write!( + f, + "OpSwitch target table has {word_len} words, which is neither valid 32-bit nor 64-bit case encoding." + ), Self::InstructionTooLong { op_len, module_remaining, diff --git a/crates/rspirv2-types/src/operand/mod.rs b/crates/rspirv2-types/src/operand/mod.rs index 387966d9..1f99b4b6 100644 --- a/crates/rspirv2-types/src/operand/mod.rs +++ b/crates/rspirv2-types/src/operand/mod.rs @@ -4,6 +4,7 @@ mod literal_float; mod literal_integer; mod literal_string; mod parameterized_bitmask; +mod switch; mod tuple; use crate::binary::{ @@ -20,6 +21,7 @@ pub use parameterized_bitmask::*; use smallvec::SmallVec; use std::fmt::{Debug, Display, Formatter}; use std::ops::Deref; +pub use switch::*; pub use tuple::*; /// A SPIR-V operand. The associated const [`Self::KIND`] links to it's [`OperandKind`]. @@ -126,10 +128,14 @@ pub unsafe trait OperandEncoding: Sized + Debug { /// Disassemble this operand /// /// Returns a type that impl [`Display`], so it's usable with `format!`: - /// ```no_run - /// # use rspirv2_types::operand::IdResult; + /// ``` + /// # use rspirv2_types::Word; + /// # use rspirv2_types::dis::{DisContext, DisOptions}; + /// # use rspirv2_types::operand::{IdResult, OperandDisContext, OperandEncoding}; + /// let ctx = DisContext::no_context(DisOptions::simple()); + /// let ctx = OperandDisContext::new(&ctx); /// let my_op = IdResult(Word(42)); - /// let dis = format!("OpMyInst {}", my_op.dis()); + /// let dis = format!("OpMyInst {}", my_op.dis(&ctx)); /// assert_eq!(dis, "OpMyInst %42"); /// ``` #[inline] diff --git a/crates/rspirv2-types/src/operand/switch.rs b/crates/rspirv2-types/src/operand/switch.rs new file mode 100644 index 00000000..b6efab9e --- /dev/null +++ b/crates/rspirv2-types/src/operand/switch.rs @@ -0,0 +1,373 @@ +use crate::Word; +use crate::binary::{DecodeError, DecodeErrorKind, EncodeError, OperandReader, WordWriter}; +use crate::operand::{ + IdRef, IdResult, LiteralConst, LiteralInteger, OperandDisContext, OperandEncoding, +}; +use smallvec::SmallVec; +use std::error::Error; +use std::fmt::{Debug, Display, Formatter}; +use std::hash::Hash; +use std::marker::PhantomData; + +/// A typed literal width for an [`OpSwitch`](https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html#OpSwitch) +/// case value. +/// +/// SPIR-V encodes each `OpSwitch` case literal with the bit width of the selector's +/// integer type. That selector type is not locally available while decoding a +/// single instruction, so [`SwitchTargets`] stores the encoded case table +/// losslessly and exposes typed views through this trait. +pub trait SwitchLiteral: private::Sealed + Copy + Debug + Eq + PartialEq + Hash { + const WORD_LEN: usize; + + fn from_words(words: &[Word]) -> Self; + + fn append_words(self, words: &mut SmallVec<[Word; 6]>); + + fn dis_fmt(&self, f: &mut Formatter<'_>, ctx: &OperandDisContext<'_>) -> std::fmt::Result; +} + +/// A one-word `OpSwitch` case literal. +#[repr(transparent)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct SwitchLiteral32(Word); + +impl SwitchLiteral32 { + #[inline] + pub const fn new(value: u32) -> Self { + Self(Word(value)) + } + + #[inline] + pub const fn from_word(value: Word) -> Self { + Self(value) + } + + #[inline] + pub const fn to_word(self) -> Word { + self.0 + } + + #[inline] + pub const fn to_u32(self) -> u32 { + self.0.0 + } +} + +impl From for SwitchLiteral32 { + #[inline] + fn from(value: u32) -> Self { + Self::new(value) + } +} + +impl private::Sealed for SwitchLiteral32 {} + +impl SwitchLiteral for SwitchLiteral32 { + const WORD_LEN: usize = 1; + + #[inline] + fn from_words(words: &[Word]) -> Self { + debug_assert_eq!(words.len(), Self::WORD_LEN); + Self(words[0]) + } + + #[inline] + fn append_words(self, words: &mut SmallVec<[Word; 6]>) { + words.push(self.0); + } + + #[inline] + fn dis_fmt(&self, f: &mut Formatter<'_>, ctx: &OperandDisContext<'_>) -> std::fmt::Result { + LiteralInteger::from_word(self.0).dis_fmt(f, ctx) + } +} + +/// A two-word `OpSwitch` case literal. +#[repr(transparent)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct SwitchLiteral64([Word; 2]); + +impl SwitchLiteral64 { + #[inline] + pub const fn new(value: u64) -> Self { + Self([Word(value as u32), Word((value >> 32) as u32)]) + } + + #[inline] + pub const fn from_words_array(words: [Word; 2]) -> Self { + Self(words) + } + + #[inline] + pub const fn to_words(self) -> [Word; 2] { + self.0 + } + + #[inline] + pub const fn to_u64(self) -> u64 { + self.0[0].0 as u64 | ((self.0[1].0 as u64) << 32) + } +} + +impl From for SwitchLiteral64 { + #[inline] + fn from(value: u64) -> Self { + Self::new(value) + } +} + +impl private::Sealed for SwitchLiteral64 {} + +impl SwitchLiteral for SwitchLiteral64 { + const WORD_LEN: usize = 2; + + #[inline] + fn from_words(words: &[Word]) -> Self { + debug_assert_eq!(words.len(), Self::WORD_LEN); + Self([words[0], words[1]]) + } + + #[inline] + fn append_words(self, words: &mut SmallVec<[Word; 6]>) { + words.extend(self.0); + } + + #[inline] + fn dis_fmt(&self, f: &mut Formatter<'_>, ctx: &OperandDisContext<'_>) -> std::fmt::Result { + LiteralConst::from(self.to_u64()).dis_fmt(f, ctx) + } +} + +/// A typed `OpSwitch` case target. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct SwitchCase { + literal: L, + target: IdRef, +} + +impl SwitchCase { + #[inline] + pub const fn new(literal: L, target: IdRef) -> Self { + Self { literal, target } + } + + #[inline] + pub const fn literal(self) -> L { + self.literal + } + + #[inline] + pub const fn target(self) -> IdRef { + self.target + } + + #[inline] + fn append_words(self, words: &mut SmallVec<[Word; 6]>) { + self.literal.append_words(words); + words.push(self.target.0.0); + } + + #[inline] + fn dis_fmt(&self, f: &mut Formatter<'_>, ctx: &OperandDisContext<'_>) -> std::fmt::Result { + self.literal.dis_fmt(f, ctx)?; + self.target.dis_fmt(f, ctx) + } +} + +/// A lossless `OpSwitch` case table. +/// +/// A decoded `OpSwitch` cannot know whether the trailing words are 32-bit or +/// 64-bit cases without looking up the selector's type elsewhere in the module. +/// This type therefore preserves the raw words while providing typed +/// constructors and typed views when that selector width is known. +#[derive(Clone, Debug, Default, Eq, PartialEq, Hash)] +pub struct SwitchTargets { + words: SmallVec<[Word; 6]>, +} + +impl SwitchTargets { + #[inline] + pub fn empty() -> Self { + Self::default() + } + + #[inline] + pub fn from_cases(cases: impl IntoIterator>) -> Self { + let mut words = SmallVec::new(); + for case in cases { + case.append_words(&mut words); + } + Self { words } + } + + #[inline] + pub fn try_from_words( + words: impl IntoIterator, + ) -> Result { + let words = SmallVec::from_iter(words); + validate_switch_target_words(words.len())?; + Ok(Self { words }) + } + + #[inline] + pub fn as_words(&self) -> &[Word] { + &self.words + } + + #[inline] + pub fn cases(&self) -> Result, InvalidSwitchTargets> { + let case_word_len = switch_case_word_len::(); + if !self.words.len().is_multiple_of(case_word_len) { + return Err(InvalidSwitchTargets::new(self.words.len())); + } + Ok(SwitchCases { + words: &self.words, + offset: 0, + _literal: PhantomData, + }) + } +} + +unsafe impl OperandEncoding for SwitchTargets { + const FIXED_LEN: Option = None; + + #[inline] + fn word_len(&self) -> usize { + self.words.len() + } + + #[inline] + fn encode(&self, writer: &mut impl WordWriter) -> Result<(), EncodeError> { + writer.write_iter(self.words.iter().copied()); + Ok(()) + } + + #[inline] + fn decode(reader: &mut OperandReader<'_>) -> Result { + Self::decode_last(reader) + } + + #[inline] + fn decode_last(reader: &mut OperandReader<'_>) -> Result { + Self::try_from_words(reader.by_ref()).map_err(DecodeError::from) + } + + #[inline] + fn dis_fmt(&self, f: &mut Formatter<'_>, ctx: &OperandDisContext<'_>) -> std::fmt::Result { + if self + .words + .len() + .is_multiple_of(switch_case_word_len::()) + { + for case in self.cases::().expect("validated above") { + case.dis_fmt(f, ctx)?; + } + } else { + for case in self + .cases::() + .expect("validated on construction") + { + case.dis_fmt(f, ctx)?; + } + } + Ok(()) + } +} + +/// A typed view over [`SwitchTargets`]. +#[derive(Clone, Debug)] +pub struct SwitchCases<'a, L: SwitchLiteral> { + words: &'a [Word], + offset: usize, + _literal: PhantomData, +} + +impl Iterator for SwitchCases<'_, L> { + type Item = SwitchCase; + + #[inline] + fn next(&mut self) -> Option { + let case_word_len = switch_case_word_len::(); + if self.offset == self.words.len() { + return None; + } + let literal = L::from_words(&self.words[self.offset..self.offset + L::WORD_LEN]); + let target = IdRef(IdResult(self.words[self.offset + L::WORD_LEN])); + self.offset += case_word_len; + Some(SwitchCase::new(literal, target)) + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + let len = self.len(); + (len, Some(len)) + } +} + +impl ExactSizeIterator for SwitchCases<'_, L> { + #[inline] + fn len(&self) -> usize { + (self.words.len() - self.offset) / switch_case_word_len::() + } +} + +/// Error returned when an `OpSwitch` target table cannot be represented as a +/// sequence of 32-bit or 64-bit cases. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct InvalidSwitchTargets { + word_len: usize, +} + +impl InvalidSwitchTargets { + #[inline] + pub const fn new(word_len: usize) -> Self { + Self { word_len } + } + + #[inline] + pub const fn word_len(self) -> usize { + self.word_len + } +} + +impl Display for InvalidSwitchTargets { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "OpSwitch target table has {} words, which is neither 32-bit nor 64-bit case encoding", + self.word_len + ) + } +} + +impl Error for InvalidSwitchTargets {} + +impl From for DecodeError { + #[inline] + fn from(value: InvalidSwitchTargets) -> Self { + DecodeErrorKind::InvalidSwitchTargets { + word_len: value.word_len, + } + .into() + } +} + +#[inline] +fn validate_switch_target_words(word_len: usize) -> Result<(), InvalidSwitchTargets> { + if word_len.is_multiple_of(switch_case_word_len::()) + || word_len.is_multiple_of(switch_case_word_len::()) + { + Ok(()) + } else { + Err(InvalidSwitchTargets::new(word_len)) + } +} + +#[inline] +fn switch_case_word_len() -> usize { + L::WORD_LEN + IdRef::FIXED_LEN.expect("IdRef has a fixed width") +} + +mod private { + pub trait Sealed {} +} diff --git a/crates/rspirv2/src/core/inst.rs b/crates/rspirv2/src/core/inst.rs index a1f60663..e655f977 100644 --- a/crates/rspirv2/src/core/inst.rs +++ b/crates/rspirv2/src/core/inst.rs @@ -12348,7 +12348,7 @@ impl InstEncoding for OpBranchConditional { pub struct OpSwitch { pub selector: IdRef, pub default: IdRef, - pub target: ZeroOrMore, + pub target: SwitchTargets, } impl Inst for OpSwitch { const META: &InstMeta = &OP_SWITCH; diff --git a/crates/rspirv2/tests/dis.rs b/crates/rspirv2/tests/dis.rs index 5048f845..6a68054a 100644 --- a/crates/rspirv2/tests/dis.rs +++ b/crates/rspirv2/tests/dis.rs @@ -5,7 +5,8 @@ use rspirv2_types::binary::IdResultAlloc; use rspirv2_types::dis::{DisContext, DisOptions}; use rspirv2_types::inst::InstEncoding; use rspirv2_types::operand::{ - IdRef, IdResultType, LiteralInteger, OperandDisContext, OperandEncoding, + IdRef, IdResultType, LiteralInteger, OperandDisContext, OperandEncoding, SwitchCase, + SwitchLiteral32, SwitchTargets, }; use smallvec::SmallVec; @@ -36,9 +37,9 @@ pub fn test_dis_composite_types() -> anyhow::Result<()> { let switch = OpSwitch { selector: IdRef(alloc.alloc_id()), default: IdRef(alloc.alloc_id()), - target: SmallVec::from_iter([ - (LiteralInteger::new(42), IdRef(alloc.alloc_id())), - (LiteralInteger::new(69), IdRef(alloc.alloc_id())), + target: SwitchTargets::from_cases([ + SwitchCase::new(SwitchLiteral32::new(42), IdRef(alloc.alloc_id())), + SwitchCase::new(SwitchLiteral32::new(69), IdRef(alloc.alloc_id())), ]), }; expect!["OpSwitch %0 %1 42 %2 69 %3"].assert_eq(&switch.dis(&ctx).to_string()); diff --git a/crates/rspirv2/tests/inst_encoding.rs b/crates/rspirv2/tests/inst_encoding.rs index fdf4650c..7b8878e5 100644 --- a/crates/rspirv2/tests/inst_encoding.rs +++ b/crates/rspirv2/tests/inst_encoding.rs @@ -1,16 +1,20 @@ use rspirv2::core::inst::{ - OpConstant, OpConvertUToF, OpDecorate, OpIAdd, OpNop, OpStore, OpTypeFloat, OpTypeInt, - OpTypePointer, OpVariable, + OpConstant, OpConvertUToF, OpDecorate, OpIAdd, OpNop, OpStore, OpSwitch, OpTypeFloat, + OpTypeInt, OpTypePointer, OpVariable, }; use rspirv2::core::inst_set::CoreInstSet; use rspirv2::core::operands::{Decoration, StorageClass}; use rspirv2::core::preamble::OpAccessChain; -use rspirv2::operand::{IdRef, IdResult, IdResultType, LiteralConst, LiteralInteger}; -use rspirv2_types::Word; -use rspirv2_types::binary::IdResultAlloc; +use rspirv2::operand::{ + IdRef, IdResult, IdResultType, LiteralConst, LiteralInteger, SwitchCase, SwitchLiteral32, + SwitchLiteral64, SwitchTargets, +}; +use rspirv2_types::binary::{DecodeErrorKind, IdResultAlloc, InstReader}; use rspirv2_types::inst::{Inst, InstEncoding}; +use rspirv2_types::module::{Module, SPIRV_MAGIC}; use rspirv2_types::slice::InstSlice; use rspirv2_types::vec::InstVec; +use rspirv2_types::{Word, cast_words_to_ne_bytes}; fn roundtrip(inst: T) where @@ -58,6 +62,111 @@ fn test_op_with_array() { test(&[]); } +#[test] +fn test_op_switch_32_bit_targets() -> anyhow::Result<()> { + roundtrip(OpSwitch { + selector: IdRef(IdResult(Word(1))), + default: IdRef(IdResult(Word(2))), + target: SwitchTargets::from_cases([ + SwitchCase::new(SwitchLiteral32::new(42), IdRef(IdResult(Word(3)))), + SwitchCase::new(SwitchLiteral32::new(69), IdRef(IdResult(Word(4)))), + ]), + }); + Ok(()) +} + +#[test] +fn test_op_switch_accepts_64_bit_targets_from_bytes() -> anyhow::Result<()> { + let switch_words = op_switch_words( + IdRef(IdResult(Word(34_589))), + IdRef(IdResult(Word(34_594))), + [ + Word(4_607_561), + Word(0), + Word(34_595), + Word(1_229_344_329), + Word(1_498_696_014), + Word(34_596), + Word(5_128_526), + Word(0), + Word(34_597), + ], + )?; + let module_words = module_words_with_insts(switch_words); + + let module = Module::::from_bytes(cast_words_to_ne_bytes(&module_words))?; + let mut iter = module.iter(); + let Some(CoreInstSet::Switch(switch)) = iter.next() else { + panic!("expected OpSwitch"); + }; + assert!(iter.next().is_none()); + + let cases = switch + .target + .cases::()? + .collect::>(); + assert_eq!( + cases.as_slice(), + &[ + SwitchCase::new( + SwitchLiteral64::new(4_607_561), + IdRef(IdResult(Word(34_595))) + ), + SwitchCase::new( + SwitchLiteral64::from_words_array([Word(1_229_344_329), Word(1_498_696_014)]), + IdRef(IdResult(Word(34_596))), + ), + SwitchCase::new( + SwitchLiteral64::new(5_128_526), + IdRef(IdResult(Word(34_597))) + ), + ] + ); + assert_eq!(&module_words[5..], module.inst.as_raw_slice().as_words()); + + Ok(()) +} + +#[test] +fn test_op_switch_rejects_invalid_target_word_count() -> anyhow::Result<()> { + let words = op_switch_words( + IdRef(IdResult(Word(1))), + IdRef(IdResult(Word(2))), + [Word(3), Word(4), Word(5), Word(6), Word(7)], + )?; + + let err = CoreInstSet::decode(InstReader::from_words(&words)?).unwrap_err(); + assert_eq!( + err.kind, + DecodeErrorKind::InvalidSwitchTargets { word_len: 5 } + ); + + Ok(()) +} + +fn op_switch_words( + selector: IdRef, + default: IdRef, + targets: impl IntoIterator, +) -> Result, rspirv2_types::binary::EncodeError> { + let mut words = Vec::from([Word(0), selector.0.0, default.0.0]); + words.extend(targets); + words[0] = Word::new_op(251, words.len())?; + Ok(words) +} + +fn module_words_with_insts(insts: Vec) -> Vec { + let mut module = Vec::from([ + SPIRV_MAGIC, + Word(0x0001_0000), + Word(0), + Word(34_598), + Word(0), + ]); + module.extend(insts); + module +} + #[test] fn test_op_constant_sequence() -> anyhow::Result<()> { let c1 = OpConstant {