#[cfg_attr(not(all(feature = "metal", target_os = "macos")), allow(dead_code))]
pub const FRAME_TABLE_SELECTOR_SLOT: u32 = 0;
#[cfg_attr(not(all(feature = "metal", target_os = "macos")), allow(dead_code))]
pub const FRAME_TABLE_DEVICE_SLOT: u32 = 1;
#[cfg_attr(
not(any(
feature = "vulkan",
all(feature = "dx12", target_os = "windows"),
all(feature = "metal", target_os = "macos"),
)),
allow(dead_code)
)]
pub const FRAME_TABLE_USER_SLOT_BASE: u32 = 2;
pub const FRAME_TABLE_ROW_STRIDE: u32 = 512;
pub const FRAME_TABLE_MAX_ROWS: u32 = 8;
pub const FRAME_TABLE_TABLE_U32S: usize = FRAME_TABLE_ROW_STRIDE as usize * FRAME_TABLE_MAX_ROWS as usize;
#[cfg_attr(not(all(feature = "metal", target_os = "macos")), allow(dead_code))]
pub const FRAME_TABLE_TABLE_BYTES: u64 = (FRAME_TABLE_TABLE_U32S * 4) as u64;
#[cfg_attr(
not(any(feature = "vulkan", all(feature = "dx12", target_os = "windows"),)),
allow(dead_code)
)]
pub const FRAME_TABLE_STAGING_SELECTOR_U32S: usize = FRAME_TABLE_MAX_ROWS as usize;
#[cfg_attr(
not(any(feature = "vulkan", all(feature = "dx12", target_os = "windows"),)),
allow(dead_code)
)]
pub const FRAME_TABLE_STAGING_U32S: usize = FRAME_TABLE_STAGING_SELECTOR_U32S + FRAME_TABLE_TABLE_U32S;
#[cfg_attr(
not(any(feature = "vulkan", all(feature = "dx12", target_os = "windows"),)),
allow(dead_code)
)]
pub const FRAME_TABLE_STAGING_BYTES: u64 = (FRAME_TABLE_STAGING_U32S * 4) as u64;
#[cfg_attr(
not(any(feature = "vulkan", all(feature = "dx12", target_os = "windows"),)),
allow(dead_code)
)]
#[inline]
pub fn staging_selector_byte_offset(row: u32) -> u64 {
(row as u64) * 4
}
#[cfg_attr(
not(any(feature = "vulkan", all(feature = "dx12", target_os = "windows"),)),
allow(dead_code)
)]
#[inline]
pub fn staging_row_payload_byte_offset(row: u32) -> u64 {
(FRAME_TABLE_STAGING_SELECTOR_U32S as u64 + row as u64 * FRAME_TABLE_ROW_STRIDE as u64) * 4
}
#[inline]
pub fn dispatch_table_base_word_index() -> usize {
0
}
#[derive(Debug, Default, Clone)]
pub struct FrameTableStaging {
pub data: Vec<u32>,
next_dispatch_base: u32,
}
impl FrameTableStaging {
pub fn new() -> Self {
Self {
data: vec![0u32; FRAME_TABLE_TABLE_U32S],
next_dispatch_base: 0,
}
}
pub fn alloc_dispatch(&mut self, slot_count: u32) -> u32 {
let base = self.next_dispatch_base;
let end = base.saturating_add(slot_count);
self.next_dispatch_base = end.min(FRAME_TABLE_ROW_STRIDE);
base
}
pub fn has_bindings(&self) -> bool {
self.next_dispatch_base > 0
}
pub fn write_dispatch_indices(&mut self, dispatch_base: u32, indices: &[u32]) {
let row = 0usize;
let row_start = row * FRAME_TABLE_ROW_STRIDE as usize;
let row_end = row_start + FRAME_TABLE_ROW_STRIDE as usize;
let base = row_start + dispatch_base as usize;
for (i, &idx) in indices.iter().enumerate() {
let pos = base + i;
if pos < row_end.min(self.data.len()) {
self.data[pos] = idx;
}
}
}
pub fn as_arc(&self) -> std::sync::Arc<[u32]> {
self.data.as_slice().into()
}
}
pub fn lower_render_pass_commands(
staging: &mut FrameTableStaging,
commands: &[crate::backend::RenderCommand],
) -> Vec<crate::backend::RenderCommand> {
commands
.iter()
.map(|cmd| match cmd {
crate::backend::RenderCommand::BindResourcesTyped { handles } => {
let indices: Vec<u32> = handles.iter().map(|h| h.index()).collect();
let frame_table_base = staging.alloc_dispatch(indices.len() as u32);
staging.write_dispatch_indices(frame_table_base, &indices);
crate::backend::RenderCommand::BindResourcesRaw {
indices: Vec::new(),
user: Vec::new(),
frame_table_base,
}
}
crate::backend::RenderCommand::BindResourcesRaw {
indices,
user,
frame_table_base: _,
} if indices.is_empty() => cmd.clone(),
crate::backend::RenderCommand::BindResourcesRaw {
indices,
user,
frame_table_base: _,
} => {
let frame_table_base = staging.alloc_dispatch(indices.len() as u32);
staging.write_dispatch_indices(frame_table_base, indices);
crate::backend::RenderCommand::BindResourcesRaw {
indices: Vec::new(),
user: user.clone(),
frame_table_base,
}
}
other => other.clone(),
})
.collect()
}
#[cfg_attr(
not(any(
feature = "vulkan",
all(feature = "dx12", target_os = "windows"),
all(feature = "metal", target_os = "macos"),
)),
allow(dead_code)
)]
pub fn lower_gpu_commands(_commands: &mut Vec<crate::backend::GpuCommand>) {}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::{GpuCommand, RenderCommand};
#[test]
fn lower_render_skips_already_lowered_raw() {
let mut staging = FrameTableStaging::new();
let cmds = vec![RenderCommand::BindResourcesRaw {
indices: vec![],
user: vec![],
frame_table_base: 3,
}];
let lowered = lower_render_pass_commands(&mut staging, &cmds);
assert_eq!(lowered.len(), 1);
assert!(matches!(
lowered[0],
RenderCommand::BindResourcesRaw {
frame_table_base: 3,
..
}
));
}
#[test]
fn lower_gpu_no_staging_for_bind_free_stream() {
let mut cmds = vec![GpuCommand::CopyTexture { src: 1u64, dst: 2u64 }];
lower_gpu_commands(&mut cmds);
assert!(
!cmds.iter().any(|c| matches!(c, GpuCommand::FrameTableStaging { .. })),
"bind-free stream must not receive a FrameTableStaging prefix"
);
}
#[test]
fn has_bindings_tracks_alloc_dispatch() {
let mut s = FrameTableStaging::new();
assert!(!s.has_bindings(), "fresh staging should have no bindings");
s.alloc_dispatch(3);
assert!(s.has_bindings(), "after alloc_dispatch staging must report bindings");
}
#[test]
fn alloc_dispatch_does_not_overflow_row_stride() {
let mut s = FrameTableStaging::new();
let full = FRAME_TABLE_ROW_STRIDE;
let base = s.alloc_dispatch(full);
assert_eq!(base, 0);
assert_eq!(s.next_dispatch_base, FRAME_TABLE_ROW_STRIDE);
let overflow_base = s.alloc_dispatch(10);
assert!(
overflow_base <= FRAME_TABLE_ROW_STRIDE,
"alloc_dispatch overflowed: base={overflow_base} > ROW_STRIDE={FRAME_TABLE_ROW_STRIDE}"
);
s.write_dispatch_indices(overflow_base, &[42u32; 10]);
}
#[test]
fn write_dispatch_indices_stays_in_row_zero() {
let mut s = FrameTableStaging::new();
let last_base = FRAME_TABLE_ROW_STRIDE - 4;
s.write_dispatch_indices(last_base, &[1, 2, 3, 4, 5, 6]); for k in 0..4usize {
assert_eq!(s.data[last_base as usize + k], k as u32 + 1);
}
for k in 0..4usize {
assert_eq!(
s.data[FRAME_TABLE_ROW_STRIDE as usize + k],
0,
"write_dispatch_indices spilled into row 1 at k={k}"
);
}
}
#[test]
fn row_stride_matches_slang_constant() {
let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
let slang_path = manifest.join("shaders/goldy_exp/access.slang");
let content =
std::fs::read_to_string(&slang_path).unwrap_or_else(|e| panic!("read {}: {e}", slang_path.display()));
let needle = "GOLDY_FRAME_TABLE_ROW_STRIDE";
let line = content
.lines()
.find(|l| l.contains(needle))
.unwrap_or_else(|| panic!("{needle} not found in {}", slang_path.display()));
let rhs = line
.split('=')
.nth(1)
.unwrap_or_else(|| panic!("expected '=' in slang line: {line}"))
.trim()
.trim_end_matches(';')
.trim();
let slang_stride: u32 = rhs
.parse()
.unwrap_or_else(|e| panic!("parse slang stride '{rhs}': {e}"));
assert_eq!(
slang_stride, FRAME_TABLE_ROW_STRIDE,
"update goldy/shaders/goldy_exp/access.slang or FRAME_TABLE_ROW_STRIDE in frame_table.rs"
);
}
#[test]
fn staging_row_payload_offsets_are_non_overlapping() {
for row in 0..FRAME_TABLE_MAX_ROWS {
let sel = staging_selector_byte_offset(row);
let payload = staging_row_payload_byte_offset(row);
assert_eq!(sel, row as u64 * 4);
assert!(
payload + (FRAME_TABLE_ROW_STRIDE as u64 * 4) <= FRAME_TABLE_STAGING_BYTES,
"row {row} payload overflows staging buffer"
);
if row > 0 {
let prev_payload_end = staging_row_payload_byte_offset(row - 1) + FRAME_TABLE_ROW_STRIDE as u64 * 4;
assert!(
payload >= prev_payload_end,
"row {row} payload overlaps row {}",
row - 1
);
}
}
}
#[test]
fn dispatch_bases_do_not_overlap_within_row() {
let mut s = FrameTableStaging::new();
let base_a = s.alloc_dispatch(5);
let base_b = s.alloc_dispatch(3);
let base_c = s.alloc_dispatch(7);
assert_eq!(base_a, 0);
assert_eq!(base_b, 5);
assert_eq!(base_c, 8);
s.write_dispatch_indices(base_a, &[10, 11, 12, 13, 14]);
s.write_dispatch_indices(base_b, &[20, 21, 22]);
assert_eq!(s.data[0], 10);
assert_eq!(s.data[4], 14);
assert_eq!(s.data[5], 20); assert_eq!(s.data[7], 22);
assert_eq!(s.data[8], 0); }
}