nak: Move SSAValue and friends to a new ssa_value.rs file

This is getting complicated and depends on some inter-linked details for
safety such as values being in-range. It's safer if the rest of the IR
is forced to use public interfaces.

Part-of: <https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/34794>
This commit is contained in:
Faith Ekstrand 2025-05-05 18:29:24 -04:00 committed by Marge Bot
parent 9d1c38ddf1
commit 56bdf9043b
4 changed files with 353 additions and 351 deletions

View file

@ -10,16 +10,15 @@ use nak_bindings::*;
pub use crate::builder::{Builder, InstrBuilder, SSABuilder, SSAInstrBuilder};
use crate::legalize::LegalizeBuilder;
use crate::sph::{OutputTopology, PixelImap};
pub use crate::ssa_value::*;
use compiler::as_slice::*;
use compiler::cfg::CFG;
use compiler::smallvec::SmallVec;
use nak_ir_proc::*;
use std::array;
use std::cmp::{max, min};
use std::fmt;
use std::fmt::Write;
use std::iter::Zip;
use std::num::NonZeroU32;
use std::ops::{BitAnd, BitOr, Deref, DerefMut, Index, IndexMut, Not, Range};
use std::slice;
@ -150,7 +149,7 @@ impl RegFile {
}
}
fn fmt_prefix(&self) -> &'static str {
pub fn fmt_prefix(&self) -> &'static str {
match self {
RegFile::GPR => "r",
RegFile::UGPR => "ur",
@ -353,336 +352,6 @@ impl<T> IndexMut<RegFile> for PerRegFile<T> {
}
}
/// An SSA value
///
/// Each SSA in NAK represents a single 32-bit or 1-bit (if a predicate) value
/// which must either be spilled to memory or allocated space in the specified
/// register file. Whenever more data is required such as a 64-bit memory
/// address, double-precision float, or a vec4 texture result, multiple SSA
/// values are used.
///
/// Each SSA value logically contains two things: an index and a register file.
/// It is required that each index refers to a unique SSA value, regardless of
/// register file. This way the index can be used to index tightly-packed data
/// structures such as bitsets without having to determine separate ranges for
/// each register file.
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct SSAValue {
packed: NonZeroU32,
}
impl SSAValue {
/// Returns an SSA value with the given register file and index
pub fn new(file: RegFile, idx: u32) -> SSAValue {
assert!(
idx > 0
&& idx < (1 << 29) - u32::try_from(SSARef::LARGE_SIZE).unwrap()
);
let mut packed = idx;
assert!(u8::from(file) < 8);
packed |= u32::from(u8::from(file)) << 29;
SSAValue {
packed: packed.try_into().unwrap(),
}
}
/// Returns the index of this SSA value
pub fn idx(&self) -> u32 {
self.packed.get() & 0x1fffffff
}
fn fmt_plain(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}{}", self.file().fmt_prefix(), self.idx())
}
}
impl HasRegFile for SSAValue {
/// Returns the register file of this SSA value
fn file(&self) -> RegFile {
RegFile::try_from(self.packed.get() >> 29).unwrap()
}
}
impl fmt::Display for SSAValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "%")?;
self.fmt_plain(f)
}
}
#[derive(Clone, Eq, Hash, PartialEq)]
struct SSAValueArray<const SIZE: usize> {
v: [SSAValue; SIZE],
}
impl<const SIZE: usize> SSAValueArray<SIZE> {
/// Returns a new SSA reference
#[inline]
fn new(comps: &[SSAValue]) -> Self {
assert!(comps.len() > 0 && comps.len() <= SIZE);
let mut r = Self {
v: [SSAValue {
packed: NonZeroU32::MAX,
}; SIZE],
};
for i in 0..comps.len() {
r.v[i] = comps[i];
}
if comps.len() < SIZE {
r.v[SIZE - 1].packed =
(comps.len() as u32).wrapping_neg().try_into().unwrap();
}
r
}
fn comps(&self) -> u8 {
let size: u8 = SIZE.try_into().unwrap();
if self.v[SIZE - 1].packed.get() >= u32::MAX - (u32::from(size) - 1) {
self.v[SIZE - 1].packed.get().wrapping_neg() as u8
} else {
size
}
}
}
impl<const SIZE: usize> Deref for SSAValueArray<SIZE> {
type Target = [SSAValue];
fn deref(&self) -> &[SSAValue] {
let comps = usize::from(self.comps());
&self.v[..comps]
}
}
impl<const SIZE: usize> DerefMut for SSAValueArray<SIZE> {
fn deref_mut(&mut self) -> &mut [SSAValue] {
let comps = usize::from(self.comps());
&mut self.v[..comps]
}
}
/// A reference to one or more SSA values
///
/// Because each SSA value represents a single 1 or 32-bit scalar, we need a way
/// to reference multiple SSA values for instructions which read or write
/// multiple registers in the same source. When the register allocator runs,
/// all the SSA values in a given SSA ref will be placed in consecutive
/// registers, with the base register aligned to the number of values, aligned
/// to the next power of two.
///
/// An SSA reference can reference between 1 and 16 SSA values. It dereferences
/// to a slice for easy access to individual SSA values. The structure is
/// designed so that is always 16B, regardless of how many SSA values are
/// referenced so it's easy and fairly cheap to clone and embed in other
/// structures.
#[derive(Clone, Eq, Hash, PartialEq)]
enum SSARefInner {
Small(SSAValueArray<{ SSARef::SMALL_SIZE }>),
Large(Box<SSAValueArray<{ SSARef::LARGE_SIZE }>>),
}
#[derive(Clone, Eq, Hash, PartialEq)]
pub struct SSARef {
v: SSARefInner,
}
#[cfg(target_arch = "x86_64")]
const _: () = {
debug_assert!(std::mem::size_of::<SSARef>() == 16);
};
impl SSARef {
const SMALL_SIZE: usize = 4;
const LARGE_SIZE: usize = 16;
/// Returns a new SSA reference
#[inline]
pub fn new(comps: &[SSAValue]) -> SSARef {
SSARef {
v: if comps.len() > Self::SMALL_SIZE {
Self::cold();
SSARefInner::Large(Box::new(SSAValueArray::new(comps)))
} else {
SSARefInner::Small(SSAValueArray::new(comps))
},
}
}
fn from_iter(mut it: impl ExactSizeIterator<Item = SSAValue>) -> Self {
let len = it.len();
assert!(len > 0 && len <= Self::LARGE_SIZE);
let v: [SSAValue; Self::LARGE_SIZE] = array::from_fn(|_| {
it.next().unwrap_or(SSAValue {
packed: NonZeroU32::MAX,
})
});
Self::new(&v[..len])
}
/// Returns the number of components in this SSA reference
pub fn comps(&self) -> u8 {
match &self.v {
SSARefInner::Small(x) => x.comps(),
SSARefInner::Large(x) => {
Self::cold();
x.comps()
}
}
}
pub fn file(&self) -> Option<RegFile> {
let comps = usize::from(self.comps());
let file = self[0].file();
for i in 1..comps {
if self[i].file() != file {
return None;
}
}
Some(file)
}
pub fn is_uniform(&self) -> bool {
for ssa in &self[..] {
if !ssa.is_uniform() {
return false;
}
}
true
}
pub fn is_gpr(&self) -> bool {
for ssa in &self[..] {
if !ssa.is_gpr() {
return false;
}
}
true
}
pub fn is_predicate(&self) -> bool {
if self[0].is_predicate() {
true
} else {
for ssa in &self[..] {
debug_assert!(!ssa.is_predicate());
}
false
}
}
#[cold]
#[inline]
fn cold() {}
}
impl Deref for SSARef {
type Target = [SSAValue];
fn deref(&self) -> &[SSAValue] {
match &self.v {
SSARefInner::Small(x) => x.deref(),
SSARefInner::Large(x) => {
Self::cold();
x.deref()
}
}
}
}
impl DerefMut for SSARef {
fn deref_mut(&mut self) -> &mut [SSAValue] {
match &mut self.v {
SSARefInner::Small(x) => x.deref_mut(),
SSARefInner::Large(x) => {
Self::cold();
x.deref_mut()
}
}
}
}
impl TryFrom<&[SSAValue]> for SSARef {
type Error = &'static str;
fn try_from(comps: &[SSAValue]) -> Result<Self, Self::Error> {
if comps.len() == 0 {
Err("Empty vector")
} else if comps.len() > Self::LARGE_SIZE {
Err("Too many vector components")
} else {
Ok(SSARef::new(comps))
}
}
}
impl TryFrom<Vec<SSAValue>> for SSARef {
type Error = &'static str;
fn try_from(comps: Vec<SSAValue>) -> Result<Self, Self::Error> {
SSARef::try_from(&comps[..])
}
}
macro_rules! impl_ssa_ref_from_arr {
($n: expr) => {
impl From<[SSAValue; $n]> for SSARef {
fn from(comps: [SSAValue; $n]) -> Self {
SSARef::new(&comps[..])
}
}
};
}
impl_ssa_ref_from_arr!(1);
impl_ssa_ref_from_arr!(2);
impl_ssa_ref_from_arr!(3);
impl_ssa_ref_from_arr!(4);
impl From<SSAValue> for SSARef {
fn from(val: SSAValue) -> Self {
[val].into()
}
}
impl fmt::Display for SSARef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.comps() == 1 {
write!(f, "{}", self[0])
} else {
write!(f, "{{")?;
for (i, v) in self.iter().enumerate() {
if i != 0 {
write!(f, " ")?;
}
write!(f, "{}", v)?;
}
write!(f, "}}")
}
}
}
pub struct SSAValueAllocator {
count: u32,
}
impl SSAValueAllocator {
pub fn new() -> SSAValueAllocator {
SSAValueAllocator { count: 0 }
}
#[allow(dead_code)]
pub fn max_idx(&self) -> u32 {
self.count
}
pub fn alloc(&mut self, file: RegFile) -> SSAValue {
self.count += 1;
SSAValue::new(file, self.count)
}
pub fn alloc_vec(&mut self, file: RegFile, comps: u8) -> SSARef {
SSARef::from_iter((0..comps).map(|_| self.alloc(file)))
}
}
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct RegRef {
packed: u32,

View file

@ -1,15 +0,0 @@
// Copyright © 2025 Valve Corporation
// SPDX-License-Identifier: MIT
use crate::ir::*;
#[test]
fn test_ssa_ref_round_trip() {
for len in 1..16 {
let vec: Vec<_> = (0..len)
.map(|i| SSAValue::new(RegFile::GPR, 1337 ^ i ^ len))
.collect();
let ssa_ref = SSARef::new(&vec);
assert!(&ssa_ref[..] == &vec[..]);
}
}

View file

@ -35,6 +35,7 @@ mod sm75_instr_latencies;
mod sm80_instr_latencies;
mod sph;
mod spill_values;
mod ssa_value;
mod to_cssa;
mod union_find;
@ -44,8 +45,5 @@ mod hw_tests;
#[cfg(test)]
mod hw_runner;
#[cfg(test)]
mod ir_tests;
#[cfg(test)]
mod nvdisasm_tests;

View file

@ -0,0 +1,350 @@
// Copyright © 2025 Collabora, Ltd.
// SPDX-License-Identifier: MIT
use crate::ir::{HasRegFile, RegFile};
use std::array;
use std::fmt;
use std::num::NonZeroU32;
use std::ops::{Deref, DerefMut};
/// An SSA value
///
/// Each SSA in NAK represents a single 32-bit or 1-bit (if a predicate) value
/// which must either be spilled to memory or allocated space in the specified
/// register file. Whenever more data is required such as a 64-bit memory
/// address, double-precision float, or a vec4 texture result, multiple SSA
/// values are used.
///
/// Each SSA value logically contains two things: an index and a register file.
/// It is required that each index refers to a unique SSA value, regardless of
/// register file. This way the index can be used to index tightly-packed data
/// structures such as bitsets without having to determine separate ranges for
/// each register file.
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct SSAValue {
packed: NonZeroU32,
}
impl SSAValue {
/// Returns an SSA value with the given register file and index
fn new(file: RegFile, idx: u32) -> SSAValue {
assert!(
idx > 0
&& idx < (1 << 29) - u32::try_from(SSARef::LARGE_SIZE).unwrap()
);
let mut packed = idx;
assert!(u8::from(file) < 8);
packed |= u32::from(u8::from(file)) << 29;
SSAValue {
packed: packed.try_into().unwrap(),
}
}
/// Returns the index of this SSA value
pub fn idx(&self) -> u32 {
self.packed.get() & 0x1fffffff
}
pub fn fmt_plain(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}{}", self.file().fmt_prefix(), self.idx())
}
}
impl HasRegFile for SSAValue {
/// Returns the register file of this SSA value
fn file(&self) -> RegFile {
RegFile::try_from(self.packed.get() >> 29).unwrap()
}
}
impl fmt::Display for SSAValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "%")?;
self.fmt_plain(f)
}
}
#[derive(Clone, Eq, Hash, PartialEq)]
struct SSAValueArray<const SIZE: usize> {
v: [SSAValue; SIZE],
}
impl<const SIZE: usize> SSAValueArray<SIZE> {
/// Returns a new SSA reference
#[inline]
fn new(comps: &[SSAValue]) -> Self {
assert!(comps.len() > 0 && comps.len() <= SIZE);
let mut r = Self {
v: [SSAValue {
packed: NonZeroU32::MAX,
}; SIZE],
};
for i in 0..comps.len() {
r.v[i] = comps[i];
}
if comps.len() < SIZE {
r.v[SIZE - 1].packed =
(comps.len() as u32).wrapping_neg().try_into().unwrap();
}
r
}
fn comps(&self) -> u8 {
let size: u8 = SIZE.try_into().unwrap();
if self.v[SIZE - 1].packed.get() >= u32::MAX - (u32::from(size) - 1) {
self.v[SIZE - 1].packed.get().wrapping_neg() as u8
} else {
size
}
}
}
impl<const SIZE: usize> Deref for SSAValueArray<SIZE> {
type Target = [SSAValue];
fn deref(&self) -> &[SSAValue] {
let comps = usize::from(self.comps());
&self.v[..comps]
}
}
impl<const SIZE: usize> DerefMut for SSAValueArray<SIZE> {
fn deref_mut(&mut self) -> &mut [SSAValue] {
let comps = usize::from(self.comps());
&mut self.v[..comps]
}
}
/// A reference to one or more SSA values
///
/// Because each SSA value represents a single 1 or 32-bit scalar, we need a way
/// to reference multiple SSA values for instructions which read or write
/// multiple registers in the same source. When the register allocator runs,
/// all the SSA values in a given SSA ref will be placed in consecutive
/// registers, with the base register aligned to the number of values, aligned
/// to the next power of two.
///
/// An SSA reference can reference between 1 and 16 SSA values. It dereferences
/// to a slice for easy access to individual SSA values. The structure is
/// designed so that is always 16B, regardless of how many SSA values are
/// referenced so it's easy and fairly cheap to clone and embed in other
/// structures.
#[derive(Clone, Eq, Hash, PartialEq)]
enum SSARefInner {
Small(SSAValueArray<{ SSARef::SMALL_SIZE }>),
Large(Box<SSAValueArray<{ SSARef::LARGE_SIZE }>>),
}
#[derive(Clone, Eq, Hash, PartialEq)]
pub struct SSARef {
v: SSARefInner,
}
#[cfg(target_arch = "x86_64")]
const _: () = {
debug_assert!(std::mem::size_of::<SSARef>() == 16);
};
impl SSARef {
const SMALL_SIZE: usize = 4;
const LARGE_SIZE: usize = 16;
/// Returns a new SSA reference
#[inline]
pub fn new(comps: &[SSAValue]) -> SSARef {
SSARef {
v: if comps.len() > Self::SMALL_SIZE {
Self::cold();
SSARefInner::Large(Box::new(SSAValueArray::new(comps)))
} else {
SSARefInner::Small(SSAValueArray::new(comps))
},
}
}
fn from_iter(mut it: impl ExactSizeIterator<Item = SSAValue>) -> Self {
let len = it.len();
assert!(len > 0 && len <= Self::LARGE_SIZE);
let v: [SSAValue; Self::LARGE_SIZE] = array::from_fn(|_| {
it.next().unwrap_or(SSAValue {
packed: NonZeroU32::MAX,
})
});
Self::new(&v[..len])
}
/// Returns the number of components in this SSA reference
pub fn comps(&self) -> u8 {
match &self.v {
SSARefInner::Small(x) => x.comps(),
SSARefInner::Large(x) => {
Self::cold();
x.comps()
}
}
}
pub fn file(&self) -> Option<RegFile> {
let comps = usize::from(self.comps());
let file = self[0].file();
for i in 1..comps {
if self[i].file() != file {
return None;
}
}
Some(file)
}
pub fn is_uniform(&self) -> bool {
for ssa in &self[..] {
if !ssa.is_uniform() {
return false;
}
}
true
}
pub fn is_gpr(&self) -> bool {
for ssa in &self[..] {
if !ssa.is_gpr() {
return false;
}
}
true
}
pub fn is_predicate(&self) -> bool {
if self[0].is_predicate() {
true
} else {
for ssa in &self[..] {
debug_assert!(!ssa.is_predicate());
}
false
}
}
#[cold]
#[inline]
fn cold() {}
}
impl Deref for SSARef {
type Target = [SSAValue];
fn deref(&self) -> &[SSAValue] {
match &self.v {
SSARefInner::Small(x) => x.deref(),
SSARefInner::Large(x) => {
Self::cold();
x.deref()
}
}
}
}
impl DerefMut for SSARef {
fn deref_mut(&mut self) -> &mut [SSAValue] {
match &mut self.v {
SSARefInner::Small(x) => x.deref_mut(),
SSARefInner::Large(x) => {
Self::cold();
x.deref_mut()
}
}
}
}
impl TryFrom<&[SSAValue]> for SSARef {
type Error = &'static str;
fn try_from(comps: &[SSAValue]) -> Result<Self, Self::Error> {
if comps.len() == 0 {
Err("Empty vector")
} else if comps.len() > Self::LARGE_SIZE {
Err("Too many vector components")
} else {
Ok(SSARef::new(comps))
}
}
}
impl TryFrom<Vec<SSAValue>> for SSARef {
type Error = &'static str;
fn try_from(comps: Vec<SSAValue>) -> Result<Self, Self::Error> {
SSARef::try_from(&comps[..])
}
}
macro_rules! impl_ssa_ref_from_arr {
($n: expr) => {
impl From<[SSAValue; $n]> for SSARef {
fn from(comps: [SSAValue; $n]) -> Self {
SSARef::new(&comps[..])
}
}
};
}
impl_ssa_ref_from_arr!(1);
impl_ssa_ref_from_arr!(2);
impl_ssa_ref_from_arr!(3);
impl_ssa_ref_from_arr!(4);
impl From<SSAValue> for SSARef {
fn from(val: SSAValue) -> Self {
[val].into()
}
}
impl fmt::Display for SSARef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.comps() == 1 {
write!(f, "{}", self[0])
} else {
write!(f, "{{")?;
for (i, v) in self.iter().enumerate() {
if i != 0 {
write!(f, " ")?;
}
write!(f, "{}", v)?;
}
write!(f, "}}")
}
}
}
#[test]
fn test_ssa_ref_round_trip() {
for len in 1..16 {
let vec: Vec<_> = (0..len)
.map(|i| SSAValue::new(RegFile::GPR, 1337 ^ i ^ len))
.collect();
let ssa_ref = SSARef::new(&vec);
assert!(&ssa_ref[..] == &vec[..]);
}
}
pub struct SSAValueAllocator {
count: u32,
}
impl SSAValueAllocator {
pub fn new() -> SSAValueAllocator {
SSAValueAllocator { count: 0 }
}
#[allow(dead_code)]
pub fn max_idx(&self) -> u32 {
self.count
}
pub fn alloc(&mut self, file: RegFile) -> SSAValue {
self.count += 1;
SSAValue::new(file, self.count)
}
pub fn alloc_vec(&mut self, file: RegFile, comps: u8) -> SSARef {
SSARef::from_iter((0..comps).map(|_| self.alloc(file)))
}
}