#![allow(dead_code)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct CfType {
pub i0: isize,
pub i1: isize,
pub z0: i32,
pub z1: i32,
pub cx0: i32,
pub cy0: i32,
pub cx1: i32,
pub cy1: i32,
}
pub const CF_LEN: usize = 256;
pub const CF_SEED_INDEX: usize = 128;
use crate::rasterizer::ScanScratch;
use crate::sky::Sky;
#[derive(Clone, Copy)]
pub struct SkyRef<'a> {
pub pixels: &'a [i32],
pub lat: &'a [i32],
pub xsiz_post: i32,
pub row_stride: i32,
}
impl<'a> SkyRef<'a> {
#[must_use]
pub fn from_sky(sky: &'a Sky) -> Self {
Self {
pixels: &sky.pixels,
lat: &sky.lat,
xsiz_post: sky.xsiz,
row_stride: sky.bpl / 4,
}
}
}
pub struct GrouscanInputs<'a> {
pub column: &'a [u8],
pub gylookup: &'a [i32],
pub gcsub: &'a [i64; 9],
pub slab_buf: &'a [u8],
pub column_offsets: &'a [u32],
pub mip_base_offsets: &'a [usize],
pub vsid: u32,
pub sky: Option<SkyRef<'a>>,
}
#[allow(dead_code)]
pub(crate) struct GrouscanState<'a> {
pub scratch: &'a mut ScanScratch,
pub column: &'a [u8],
pub gylookup: &'a [i32],
pub gcsub: &'a [i64; 9],
pub slab_buf: &'a [u8],
pub column_offsets: &'a [u32],
pub mip_base_offsets: &'a [usize],
pub vsid: u32,
pub sky: Option<SkyRef<'a>>,
pub z0: i32,
pub z1: i32,
pub cx0: i32,
pub cy0: i32,
pub cx1: i32,
pub cy1: i32,
pub ogx: i32,
pub gx: i32,
pub ngxmax: i32,
pub lane: usize,
pub color: u32,
pub gy_raw: i32,
pub off: i32,
pub mm5_tail: u32,
pub wall_lane: usize,
pub ebx: isize,
pub vptr_offset: usize,
pub c_idx: usize,
pub ce_idx: usize,
pub c_presync_idx: usize,
pub ixy_sptr_col_idx: usize,
pub gmipcnt: i32,
pub gmipnum: u32,
}
impl<'a> GrouscanState<'a> {
fn from_seed(
scratch: &'a mut ScanScratch,
inputs: &GrouscanInputs<'a>,
vptr_offset: usize,
ixy_sptr_col_idx: usize,
gmipnum: u32,
) -> Self {
let c = scratch.cf[CF_SEED_INDEX];
Self {
scratch,
column: inputs.column,
gylookup: inputs.gylookup,
gcsub: inputs.gcsub,
slab_buf: inputs.slab_buf,
column_offsets: inputs.column_offsets,
mip_base_offsets: inputs.mip_base_offsets,
vsid: inputs.vsid,
sky: inputs.sky,
z0: c.z0,
z1: c.z1,
cx0: c.cx0,
cy0: c.cy0,
cx1: c.cx1,
cy1: c.cy1,
ogx: 0,
gx: 0,
ngxmax: 0,
lane: 0,
color: 0,
gy_raw: 0,
off: 0,
mm5_tail: 0,
wall_lane: 0,
ebx: 0,
vptr_offset,
c_idx: CF_SEED_INDEX,
ce_idx: CF_SEED_INDEX,
c_presync_idx: usize::MAX,
ixy_sptr_col_idx,
gmipcnt: 0,
gmipnum,
}
}
}
#[allow(clippy::cast_possible_truncation)]
#[must_use]
pub fn grouscan_shade(vox: u32, tail: &mut u32, csub_qword: i64) -> u32 {
let cs = csub_qword.to_le_bytes();
let t = *tail;
let mut b = [
t as u8,
vox as u8,
(t >> 8) as u8,
(vox >> 8) as u8,
(t >> 16) as u8,
(vox >> 16) as u8,
(t >> 24) as u8,
(vox >> 24) as u8,
];
for i in 0..8 {
b[i] = b[i].saturating_sub(cs[i]);
}
let mut w = [
u16::from(b[0]) | (u16::from(b[1]) << 8),
u16::from(b[2]) | (u16::from(b[3]) << 8),
u16::from(b[4]) | (u16::from(b[5]) << 8),
u16::from(b[6]) | (u16::from(b[7]) << 8),
];
let repl = u32::from(w[3]);
for slot in &mut w {
*slot = ((u32::from(*slot) * repl) >> 16) as u16;
}
for slot in &mut w {
*slot >>= 7;
}
let p = w.map(|x| if x > 255 { 255 } else { x as u8 });
let color = u32::from(p[0])
| (u32::from(p[1]) << 8)
| (u32::from(p[2]) << 16)
| (u32::from(p[3]) << 24);
*tail = color;
color
}
#[allow(clippy::cast_possible_truncation, clippy::similar_names)]
#[must_use]
pub fn grouscan_cross_sign(cx: i32, cy: i32, depth: i32, gy_raw: i32) -> i32 {
let gy_s16 = i32::from(gy_raw as i16);
let depth_s16 = i32::from((depth >> 16) as i16);
let cx_s16 = i32::from((cx >> 16) as i16);
let cy_s16 = i32::from((cy >> 16) as i16);
cx_s16 * gy_s16 + cy_s16 * depth_s16
}
#[derive(Debug, Clone, Copy)]
pub struct GrouscanPrologue {
pub z0: i32,
pub z1: i32,
pub cx0: i32,
pub cy0: i32,
pub cx1: i32,
pub cy1: i32,
pub lane: usize,
pub ogx: i32,
pub gx: i32,
pub ngxmax: i32,
pub dispatch: InitialDispatch,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InitialDispatch {
DrawFlor,
DrawCeil,
}
#[must_use]
pub fn grouscan_run(
scratch: &mut ScanScratch,
inputs: &GrouscanInputs<'_>,
vptr_offset: usize,
ixy_sptr_col_idx: usize,
gxmip: i32,
gmipnum: u32,
) -> GrouscanPrologue {
let mut state =
GrouscanState::from_seed(scratch, inputs, vptr_offset, ixy_sptr_col_idx, gmipnum);
state.ngxmax = state.scratch.gxmax;
if gmipnum > 1 && gxmip < state.ngxmax {
state.ngxmax = gxmip;
}
state.lane = usize::from(state.scratch.gpz[1] < state.scratch.gpz[0]);
state.ogx = state.scratch.gpz[state.lane] & -0x1_0000_i32;
state.gx = 0;
state.scratch.gpz[state.lane] =
state.scratch.gpz[state.lane].wrapping_add(state.scratch.gdz[state.lane]);
let dispatch = if state.vptr_offset == 0 {
InitialDispatch::DrawFlor
} else {
InitialDispatch::DrawCeil
};
let prologue = GrouscanPrologue {
z0: state.z0,
z1: state.z1,
cx0: state.cx0,
cy0: state.cy0,
cx1: state.cx1,
cy1: state.cy1,
lane: state.lane,
ogx: state.ogx,
gx: state.gx,
ngxmax: state.ngxmax,
dispatch,
};
let entry = match dispatch {
InitialDispatch::DrawFlor => Phase::DrawFlor,
InitialDispatch::DrawCeil => Phase::DrawCeil,
};
run_phases(&mut state, entry);
prologue
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Phase {
DrawFwall,
DrawCwall,
PreDrawCeil,
DrawCeil,
PreDrawFlor,
DrawFlor,
PreDeleteZ,
DeleteZ,
AfterDelete,
AfterDeleteKeptPresync,
SkipixyWithPresync,
SyncFromPresync,
Skipixy3,
Intoslabloop,
Findslabloop,
Remiporend,
Startsky,
Done,
}
fn run_phases(state: &mut GrouscanState<'_>, entry: Phase) {
let trace = std::env::var("ROXLAP_TRACE_PHASES").is_ok();
let mut current = entry;
let mut step_count = 0u32;
loop {
if trace {
eprintln!(
" phase {step_count:4}: {current:?} c={} ce={} z0={} z1={} cx1={} cy1={} ogx={} gx={}",
state.c_idx, state.ce_idx, state.z0, state.z1, state.cx1, state.cy1, state.ogx, state.gx,
);
step_count += 1;
if step_count > 200 {
eprintln!(" (truncated)");
break;
}
}
current = match current {
Phase::DrawFwall => phase_draw_fwall(state),
Phase::DrawCwall => phase_draw_cwall(state),
Phase::PreDrawCeil => phase_pre_draw_ceil(state),
Phase::DrawCeil => phase_draw_ceil(state),
Phase::PreDrawFlor => phase_pre_draw_flor(state),
Phase::DrawFlor => phase_draw_flor(state),
Phase::PreDeleteZ => phase_pre_delete_z(state),
Phase::DeleteZ => phase_delete_z(state),
Phase::AfterDelete => phase_after_delete(state),
Phase::AfterDeleteKeptPresync => phase_after_delete_kept_presync(state),
Phase::SkipixyWithPresync => phase_skipixy_with_presync(state),
Phase::SyncFromPresync => phase_sync_from_presync(state),
Phase::Skipixy3 => phase_skipixy3(state),
Phase::Intoslabloop => phase_intoslabloop(state),
Phase::Findslabloop => phase_findslabloop(state),
Phase::Remiporend => phase_remiporend(state),
Phase::Startsky => phase_startsky(state),
Phase::Done => break,
};
}
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::similar_names
)]
fn phase_draw_fwall(state: &mut GrouscanState<'_>) -> Phase {
if state.vptr_offset + 4 > state.column.len() {
return Phase::DrawCwall;
}
let dv1 = i32::from(state.column[state.vptr_offset + 1]);
if dv1 >= state.z1 {
return Phase::DrawCwall;
}
state.ebx = state.scratch.cf[state.c_idx].i1;
'outer: loop {
state.off = state.z1 - i32::from(state.column[state.vptr_offset + 1]);
state.z1 -= 1;
let row_offset = state.vptr_offset + (state.off as usize) * 4;
if row_offset + 4 > state.column.len() {
state.scratch.cf[state.c_idx].i1 = state.ebx;
return Phase::DrawCwall;
}
let vox = u32::from_le_bytes(
state.column[row_offset..row_offset + 4]
.try_into()
.expect("4-byte slice"),
);
state.color = grouscan_shade(vox, &mut state.mm5_tail, state.gcsub[state.wall_lane]);
let z1_idx = state.z1 as usize;
if z1_idx >= state.gylookup.len() {
state.scratch.cf[state.c_idx].i1 = state.ebx;
return Phase::DrawCwall;
}
state.gy_raw = state.gylookup[z1_idx];
loop {
let test = grouscan_cross_sign(state.cx1, state.cy1, state.ogx, state.gy_raw);
if test <= 0 {
if i32::from(state.column[state.vptr_offset + 1]) != state.z1 {
continue 'outer;
}
state.scratch.cf[state.c_idx].i1 = state.ebx;
return Phase::DrawCwall;
}
state.cx1 = state.cx1.wrapping_sub(state.scratch.gi0);
state.cy1 = state.cy1.wrapping_sub(state.scratch.gi1);
let radar_idx = state.ebx as usize;
if let Some(slot) = state.scratch.radar.get_mut(radar_idx) {
slot.col = state.color as i32;
slot.dist = state.ogx;
}
state.ebx -= 1;
if state.ebx < state.scratch.cf[state.c_idx].i0 {
return Phase::PreDeleteZ;
}
}
}
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::similar_names
)]
fn phase_draw_cwall(state: &mut GrouscanState<'_>) -> Phase {
if state.vptr_offset + 4 > state.column.len() {
return Phase::PreDrawCeil;
}
state.z1 = i32::from(state.column[state.vptr_offset + 1]);
if state.vptr_offset == 0 {
return Phase::PreDrawFlor;
}
let dv3 = i32::from(state.column[state.vptr_offset + 3]);
if dv3 <= state.z0 {
state.z0 = dv3;
return Phase::PreDrawCeil;
}
state.ebx = state.scratch.cf[state.c_idx].i0;
'outer: loop {
state.off = state.z0 - i32::from(state.column[state.vptr_offset + 3]);
state.z0 += 1;
let row_offset_signed = state.vptr_offset as isize + (state.off as isize) * 4;
if row_offset_signed < 0 || (row_offset_signed as usize) + 4 > state.column.len() {
state.scratch.cf[state.c_idx].i0 = state.ebx;
state.z0 = i32::from(state.column[state.vptr_offset + 3]);
return Phase::PreDrawCeil;
}
let row_offset = row_offset_signed as usize;
let vox = u32::from_le_bytes(
state.column[row_offset..row_offset + 4]
.try_into()
.expect("4-byte slice"),
);
state.color = grouscan_shade(vox, &mut state.mm5_tail, state.gcsub[state.wall_lane]);
let z0_idx = state.z0 as usize;
if z0_idx >= state.gylookup.len() {
state.scratch.cf[state.c_idx].i0 = state.ebx;
state.z0 = i32::from(state.column[state.vptr_offset + 3]);
return Phase::PreDrawCeil;
}
state.gy_raw = state.gylookup[z0_idx];
loop {
let test = grouscan_cross_sign(state.cx0, state.cy0, state.ogx, state.gy_raw);
if test > 0 {
if i32::from(state.column[state.vptr_offset + 3]) != state.z0 {
continue 'outer;
}
state.scratch.cf[state.c_idx].i0 = state.ebx;
state.z0 = i32::from(state.column[state.vptr_offset + 3]);
return Phase::PreDrawCeil;
}
state.cx0 = state.cx0.wrapping_add(state.scratch.gi0);
state.cy0 = state.cy0.wrapping_add(state.scratch.gi1);
let radar_idx = state.ebx as usize;
if let Some(slot) = state.scratch.radar.get_mut(radar_idx) {
slot.col = state.color as i32;
slot.dist = state.ogx;
}
state.ebx += 1;
if state.ebx > state.scratch.cf[state.c_idx].i1 {
return Phase::PreDeleteZ;
}
}
}
}
fn phase_pre_draw_ceil(state: &mut GrouscanState<'_>) -> Phase {
std::mem::swap(&mut state.ogx, &mut state.gx);
Phase::DrawCeil
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::similar_names
)]
fn phase_draw_ceil(state: &mut GrouscanState<'_>) -> Phase {
let z0_idx = state.z0 as usize;
if z0_idx >= state.gylookup.len() {
return Phase::PreDeleteZ;
}
state.gy_raw = state.gylookup[z0_idx];
if state.vptr_offset < 4 {
return Phase::PreDeleteZ;
}
let vox_off = state.vptr_offset - 4;
if vox_off + 4 > state.column.len() {
return Phase::PreDeleteZ;
}
let vox = u32::from_le_bytes(
state.column[vox_off..vox_off + 4]
.try_into()
.expect("4-byte slice"),
);
loop {
let test = grouscan_cross_sign(state.cx0, state.cy0, state.ogx, state.gy_raw);
if test > 0 {
return Phase::DrawFlor;
}
state.cx0 = state.cx0.wrapping_add(state.scratch.gi0);
state.cy0 = state.cy0.wrapping_add(state.scratch.gi1);
state.color = grouscan_shade(vox, &mut state.mm5_tail, state.gcsub[2]);
let i0 = state.scratch.cf[state.c_idx].i0;
if let Some(slot) = state.scratch.radar.get_mut(i0 as usize) {
slot.col = state.color as i32;
slot.dist = state.ogx;
}
state.scratch.cf[state.c_idx].i0 = i0 + 1;
if state.scratch.cf[state.c_idx].i0 > state.scratch.cf[state.c_idx].i1 {
return Phase::DeleteZ;
}
}
}
fn phase_pre_draw_flor(state: &mut GrouscanState<'_>) -> Phase {
std::mem::swap(&mut state.ogx, &mut state.gx);
Phase::DrawFlor
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::similar_names
)]
fn phase_draw_flor(state: &mut GrouscanState<'_>) -> Phase {
let z1_idx = state.z1 as usize;
if z1_idx >= state.gylookup.len() {
return Phase::PreDeleteZ;
}
state.gy_raw = state.gylookup[z1_idx];
let vox_off = state.vptr_offset + 4;
if vox_off + 4 > state.column.len() {
return Phase::PreDeleteZ;
}
let vox = u32::from_le_bytes(
state.column[vox_off..vox_off + 4]
.try_into()
.expect("4-byte slice"),
);
loop {
let test = grouscan_cross_sign(state.cx1, state.cy1, state.ogx, state.gy_raw);
if test <= 0 {
return Phase::AfterDelete;
}
state.cx1 = state.cx1.wrapping_sub(state.scratch.gi0);
state.cy1 = state.cy1.wrapping_sub(state.scratch.gi1);
state.color = grouscan_shade(vox, &mut state.mm5_tail, state.gcsub[3]);
let i1 = state.scratch.cf[state.c_idx].i1;
if let Some(slot) = state.scratch.radar.get_mut(i1 as usize) {
slot.col = state.color as i32;
slot.dist = state.ogx;
}
state.scratch.cf[state.c_idx].i1 = i1 - 1;
if state.scratch.cf[state.c_idx].i1 < state.scratch.cf[state.c_idx].i0 {
return Phase::DeleteZ;
}
}
}
fn phase_pre_delete_z(state: &mut GrouscanState<'_>) -> Phase {
std::mem::swap(&mut state.ogx, &mut state.gx);
Phase::DeleteZ
}
fn phase_delete_z(state: &mut GrouscanState<'_>) -> Phase {
if state.ce_idx <= CF_SEED_INDEX {
return Phase::Done;
}
let old_ce = state.ce_idx;
state.ce_idx -= 1;
if state.c_idx < old_ce {
for p in state.c_idx..old_ce {
state.scratch.cf[p] = state.scratch.cf[p + 1];
}
state.c_presync_idx = old_ce;
return Phase::AfterDeleteKeptPresync;
}
Phase::AfterDelete
}
fn phase_after_delete(state: &mut GrouscanState<'_>) -> Phase {
state.c_presync_idx = state.c_idx;
Phase::AfterDeleteKeptPresync
}
#[allow(
clippy::cast_sign_loss,
clippy::cast_possible_wrap,
clippy::similar_names
)]
fn phase_after_delete_kept_presync(state: &mut GrouscanState<'_>) -> Phase {
if state.c_idx == 0 {
return Phase::Done;
}
state.c_idx -= 1;
if state.c_idx >= CF_SEED_INDEX {
return Phase::SkipixyWithPresync;
}
state.wall_lane = state.lane;
let step = state.scratch.gixy[state.lane] as isize;
state.ixy_sptr_col_idx = state.ixy_sptr_col_idx.wrapping_add_signed(step);
if let Some(&col_off) = state.column_offsets.get(state.ixy_sptr_col_idx) {
let off = col_off as usize;
if off <= state.slab_buf.len() {
state.column = &state.slab_buf[off..];
}
}
state.vptr_offset = 0;
state.lane = usize::from(state.scratch.gpz[1] < state.scratch.gpz[0]);
let new_gpz = state.scratch.gpz[state.lane];
state.gx = new_gpz & -0x1_0000_i32;
if (new_gpz as u32) > (state.ngxmax as u32) {
return Phase::Remiporend;
}
state.scratch.gpz[state.lane] =
state.scratch.gpz[state.lane].wrapping_add(state.scratch.gdz[state.lane]);
state.c_idx = state.ce_idx;
if state.c_presync_idx == state.c_idx {
Phase::Skipixy3
} else {
Phase::SyncFromPresync
}
}
fn phase_skipixy_with_presync(state: &mut GrouscanState<'_>) -> Phase {
std::mem::swap(&mut state.ogx, &mut state.gx);
Phase::SyncFromPresync
}
fn phase_sync_from_presync(state: &mut GrouscanState<'_>) -> Phase {
if state.c_presync_idx < state.scratch.cf.len() {
let presync = &mut state.scratch.cf[state.c_presync_idx];
presync.z0 = state.z0;
presync.z1 = state.z1;
presync.cx0 = state.cx0;
presync.cy0 = state.cy0;
presync.cx1 = state.cx1;
presync.cy1 = state.cy1;
}
let c = state.scratch.cf[state.c_idx];
state.z0 = c.z0;
state.z1 = c.z1;
state.cx0 = c.cx0;
state.cy0 = c.cy0;
state.cx1 = c.cx1;
state.cy1 = c.cy1;
Phase::Skipixy3
}
fn phase_skipixy3(state: &mut GrouscanState<'_>) -> Phase {
let v0 = column_byte_at(state, 0);
if v0 == 0 {
Phase::DrawFwall
} else {
Phase::Intoslabloop
}
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::similar_names
)]
fn phase_intoslabloop(state: &mut GrouscanState<'_>) -> Phase {
let v2 = i32::from(column_byte_at(state, 2));
let gy_idx = (v2 + 1) as usize;
if gy_idx >= state.gylookup.len() {
return Phase::DrawFwall;
}
state.gy_raw = state.gylookup[gy_idx];
let test_hi = grouscan_cross_sign(state.cx0, state.cy0, state.ogx, state.gy_raw);
if test_hi > 0 {
return Phase::Findslabloop;
}
let v0 = i32::from(column_byte_at(state, 0));
let next_v3_offset = (v0 * 4 + 3) as usize;
let next_v3 = i32::from(column_byte_at(state, next_v3_offset));
let next_gy_idx = next_v3 as usize;
if next_gy_idx >= state.gylookup.len() {
return Phase::DrawFwall;
}
state.gy_raw = state.gylookup[next_gy_idx];
let test_next = grouscan_cross_sign(state.cx1, state.cy1, state.ogx, state.gy_raw);
if test_next <= 0 {
return Phase::DrawFwall;
}
do_slab_split(state, v2, next_v3)
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::similar_names
)]
fn do_slab_split(state: &mut GrouscanState<'_>, v2: i32, next_v3: i32) -> Phase {
{
let z0 = state.z0;
let z1 = state.z1;
let cx0 = state.cx0;
let cy0 = state.cy0;
let cx1 = state.cx1;
let cy1 = state.cy1;
let c = &mut state.scratch.cf[state.c_idx];
c.z0 = z0;
c.z1 = z1;
c.cx0 = cx0;
c.cy0 = cy0;
c.cx1 = cx1;
c.cy1 = cy1;
}
let gy_idx = (v2 + 1) as usize;
if gy_idx >= state.gylookup.len() {
return Phase::DrawFwall;
}
state.gy_raw = state.gylookup[gy_idx];
let mut col = state.scratch.cf[state.c_idx].i1;
let i0 = state.scratch.cf[state.c_idx].i0;
let span = (col - i0).max(0) as usize;
let gi0_16 = state.scratch.gi0 << 4;
let gi1_16 = state.scratch.gi1 << 4;
let big_step_max = span / 16 + 1;
for _ in 0..big_step_max {
let cx_try = state.cx1.wrapping_sub(gi0_16);
let cy_try = state.cy1.wrapping_sub(gi1_16);
if grouscan_cross_sign(cx_try, cy_try, state.ogx, state.gy_raw) <= 0 {
break;
}
state.cx1 = cx_try;
state.cy1 = cy_try;
col -= 16;
}
for _ in 0..=16 {
if grouscan_cross_sign(state.cx1, state.cy1, state.ogx, state.gy_raw) <= 0 {
break;
}
state.cx1 = state.cx1.wrapping_sub(state.scratch.gi0);
state.cy1 = state.cy1.wrapping_sub(state.scratch.gi1);
col -= 1;
}
if state.ce_idx >= 191 {
return Phase::Done;
}
state.ce_idx += 1;
for p in (state.c_idx + 1..=state.ce_idx).rev() {
state.scratch.cf[p] = state.scratch.cf[p - 1];
}
state.scratch.cf[state.c_idx + 1].i1 = col;
{
let new_cx0 = state.cx1.wrapping_add(state.scratch.gi0);
let new_cy0 = state.cy1.wrapping_add(state.scratch.gi1);
let c = &mut state.scratch.cf[state.c_idx];
c.i0 = col + 1;
c.z0 = next_v3;
c.cx0 = new_cx0;
c.cy0 = new_cy0;
}
state.c_idx += 1;
state.z0 = state.scratch.cf[state.c_idx].z0;
state.z1 = next_v3;
Phase::DrawFwall
}
fn phase_findslabloop(state: &mut GrouscanState<'_>) -> Phase {
let v0 = column_byte_at(state, 0);
if v0 == 0 {
return Phase::DrawFwall;
}
state.vptr_offset = state.vptr_offset.saturating_add(usize::from(v0) * 4);
let next_v0 = column_byte_at(state, 0);
if next_v0 == 0 {
Phase::DrawFwall
} else {
Phase::Intoslabloop
}
}
fn column_byte_at(state: &GrouscanState<'_>, offset: usize) -> u8 {
state
.column
.get(state.vptr_offset.saturating_add(offset))
.copied()
.unwrap_or(0)
}
#[allow(clippy::cast_sign_loss, clippy::cast_possible_wrap)]
fn phase_remiporend(state: &mut GrouscanState<'_>) -> Phase {
if std::env::var("ROXLAP_TRACE_STARTSKY").is_ok() {
eprintln!(
"remiporend: gmipcnt={} gmipnum={} ce={} c={} gpz=[{}, {}] gxmax={} ngxmax={}",
state.gmipcnt,
state.gmipnum,
state.ce_idx,
state.c_idx,
state.scratch.gpz[0],
state.scratch.gpz[1],
state.scratch.gxmax,
state.ngxmax,
);
}
if (state.gmipcnt + 1) as u32 >= state.gmipnum {
return Phase::Startsky;
}
let old_mip = state.gmipcnt as usize;
state.gmipcnt += 1;
let new_mip = state.gmipcnt as usize;
let mip_old_base = state.mip_base_offsets[old_mip];
let mip_new_base = state.mip_base_offsets[new_mip];
let col_within_old = state.ixy_sptr_col_idx - mip_old_base;
let vsid_old = (state.vsid >> old_mip) as usize;
debug_assert!(vsid_old.is_power_of_two() && vsid_old > 0);
let log2_vsid_old = vsid_old.trailing_zeros() as usize;
let x_parity = col_within_old & 1;
let y_parity = (col_within_old >> log2_vsid_old) & 1;
{
let dz = state.scratch.gdz[0];
let trailing = (x_parity == 0) == (state.scratch.gixy[0] >= 0);
if trailing {
state.scratch.gpz[0] = state.scratch.gpz[0].wrapping_add(dz);
}
let doubled = dz.wrapping_add(dz);
if (dz ^ doubled) < 0 {
state.scratch.gpz[0] = i32::MAX;
state.scratch.gdz[0] = 0;
} else {
state.scratch.gdz[0] = doubled;
}
}
if state.c_presync_idx < state.scratch.cf.len() {
state.scratch.cf[state.c_presync_idx].z0 = state.z0;
}
{
let dz = state.scratch.gdz[1];
let trailing = (y_parity == 0) == (state.scratch.gixy[1] >= 0);
if trailing {
state.scratch.gpz[1] = state.scratch.gpz[1].wrapping_add(dz);
}
let doubled = dz.wrapping_add(dz);
if (dz ^ doubled) < 0 {
state.scratch.gpz[1] = i32::MAX;
state.scratch.gdz[1] = 0;
} else {
state.scratch.gdz[1] = doubled;
}
}
{
let x_old = col_within_old & (vsid_old - 1);
let y_old = col_within_old >> log2_vsid_old;
let x_new = x_old >> 1;
let y_new = y_old >> 1;
let vsid_new = vsid_old >> 1;
state.ixy_sptr_col_idx = mip_new_base + y_new * vsid_new + x_new;
}
{
let advance = ((512u32 >> old_mip) as usize) + 4;
let advance = advance.min(state.gylookup.len());
state.gylookup = &state.gylookup[advance..];
}
state.scratch.gixy[1] >>= 1;
for idx in CF_SEED_INDEX..=state.ce_idx {
let entry = &mut state.scratch.cf[idx];
entry.z0 = (entry.z0 as u32 >> 1) as i32;
entry.z1 = ((entry.z1 + 1) as u32 >> 1) as i32;
}
let gxmax = state.scratch.gxmax;
if (state.ngxmax as u32) >= (gxmax as u32) {
return Phase::Startsky;
}
let dn = state.ngxmax.wrapping_add(state.ngxmax);
state.ngxmax = if dn < 0 || dn >= gxmax { gxmax } else { dn };
if state.c_presync_idx < state.scratch.cf.len() {
state.z0 = state.scratch.cf[state.c_presync_idx].z0 >> 1;
}
state.z1 = ((state.z1 + 1) as u32 >> 1) as i32;
state.lane = usize::from(state.scratch.gpz[1] < state.scratch.gpz[0]);
let new_gpz = state.scratch.gpz[state.lane];
state.gx = new_gpz & -0x1_0000_i32;
state.scratch.gpz[state.lane] =
state.scratch.gpz[state.lane].wrapping_add(state.scratch.gdz[state.lane]);
if let Some(&col_off) = state.column_offsets.get(state.ixy_sptr_col_idx) {
let col_off = col_off as usize;
state.column = state.slab_buf.get(col_off..).unwrap_or(&[]);
}
state.c_idx = state.ce_idx;
Phase::SyncFromPresync
}
#[allow(clippy::cast_sign_loss)]
fn phase_startsky(state: &mut GrouscanState<'_>) -> Phase {
if CF_SEED_INDEX > state.ce_idx {
return Phase::Done;
}
let textured = state.sky.is_some() && state.scratch.sky_off != 0;
if textured {
phase_startsky_textured(state)
} else {
phase_startsky_solid(state)
}
}
#[allow(clippy::cast_sign_loss)]
fn phase_startsky_solid(state: &mut GrouscanState<'_>) -> Phase {
let trace = std::env::var("ROXLAP_TRACE_STARTSKY").is_ok();
let skycast = state.scratch.skycast;
for c_idx in CF_SEED_INDEX..=state.ce_idx {
let i0 = state.scratch.cf[c_idx].i0;
let i1 = state.scratch.cf[c_idx].i1;
if i0 > i1 {
if trace {
eprintln!(
"startsky cf[{c_idx}] i0={i0} i1={i1} (empty, skip; ce={})",
state.ce_idx
);
}
continue;
}
if trace {
eprintln!(
"startsky cf[{c_idx}] drains slots [{i0}..={i1}] ({} slots; ce={})",
i1 - i0 + 1,
state.ce_idx
);
}
for p in i0..=i1 {
if let Some(slot) = state.scratch.radar.get_mut(p as usize) {
*slot = skycast;
}
}
}
Phase::Done
}
#[allow(clippy::cast_sign_loss, clippy::cast_possible_wrap)]
fn phase_startsky_textured(state: &mut GrouscanState<'_>) -> Phase {
let sky = state.sky.expect("phase_startsky_textured requires SkyRef");
let sky_off = state.scratch.sky_off;
let skydist = state.scratch.skycast.dist;
let gi0 = state.scratch.gi0;
let gi1 = state.scratch.gi1;
let row_pixel_base = (sky_off as usize) / 4;
let mut sky_edi: i32 = sky.xsiz_post;
for c_idx in CF_SEED_INDEX..=state.ce_idx {
let i0 = state.scratch.cf[c_idx].i0;
let i1 = state.scratch.cf[c_idx].i1;
if i0 > i1 {
continue;
}
#[allow(clippy::cast_possible_truncation)]
let leng_remaining = (i1 - i0) as i32;
let cx0 = state.scratch.cf[c_idx].cx0;
let cy0 = state.scratch.cf[c_idx].cy0;
let mut sx = cx0.wrapping_add(leng_remaining.wrapping_mul(gi0));
let mut sy = cy0.wrapping_add(leng_remaining.wrapping_mul(gi1));
let mut p = i1;
loop {
sx = sx.wrapping_sub(gi0);
sy = sy.wrapping_sub(gi1);
loop {
if sky_edi < 0 || (sky_edi as usize) >= sky.lat.len() {
sky_edi = 0;
break;
}
let sl = sky.lat[sky_edi as usize];
let neg_yvi = i32::from((sl & 0xffff) as i16);
let xvi_lane = i32::from(((sl >> 16) & 0xffff) as i16);
let test = (sx >> 16).wrapping_mul(neg_yvi) + (sy >> 16).wrapping_mul(xvi_lane);
if test >= 0 {
break;
}
sky_edi -= 1;
}
let pixel_idx = row_pixel_base + sky_edi as usize;
let col = if pixel_idx < sky.pixels.len() {
sky.pixels[pixel_idx]
} else {
0
};
if let Some(slot) = state.scratch.radar.get_mut(p as usize) {
slot.col = col;
slot.dist = skydist;
}
if p <= i0 {
break;
}
p -= 1;
}
}
Phase::Done
}
#[cfg(test)]
mod tests {
use super::*;
fn fresh_scratch() -> ScanScratch {
let mut s = ScanScratch::new_for_size(64, 64, 64);
s.cf[CF_SEED_INDEX] = CfType {
i0: 10,
i1: 20,
z0: 5,
z1: 50,
cx0: 100,
cy0: 200,
cx1: 300,
cy1: 400,
};
s
}
const DUMMY_GYLOOKUP: [i32; 64] = [0; 64];
const DUMMY_GCSUB: [i64; 9] = [0; 9];
const DUMMY_COLUMN: [u8; 4] = [0, 0, 0, 0];
const DUMMY_SLAB_BUF: [u8; 0] = [];
const DUMMY_COLUMN_OFFSETS: [u32; 0] = [];
const DUMMY_MIP_OFFSETS: [usize; 2] = [0, 0];
fn dummy_inputs<'a>() -> GrouscanInputs<'a> {
GrouscanInputs {
column: &DUMMY_COLUMN,
gylookup: &DUMMY_GYLOOKUP,
gcsub: &DUMMY_GCSUB,
slab_buf: &DUMMY_SLAB_BUF,
column_offsets: &DUMMY_COLUMN_OFFSETS,
mip_base_offsets: &DUMMY_MIP_OFFSETS,
vsid: 64,
sky: None,
}
}
#[test]
fn prologue_caches_cf_seed_state() {
let mut s = fresh_scratch();
s.gpz = [1_000, 2_000];
s.gdz = [10, 20];
s.gxmax = 999_999;
let p = grouscan_run(&mut s, &dummy_inputs(), 0, 0, 50_000, 1);
assert_eq!(p.z0, 5);
assert_eq!(p.z1, 50);
assert_eq!(p.cx0, 100);
assert_eq!(p.cy0, 200);
assert_eq!(p.cx1, 300);
assert_eq!(p.cy1, 400);
}
#[test]
fn prologue_picks_leading_lane_with_smaller_gpz() {
let mut s = fresh_scratch();
s.gpz = [1_000, 2_000];
s.gdz = [10, 20];
let p = grouscan_run(&mut s, &dummy_inputs(), 0, 0, 1, 1);
assert_eq!(p.lane, 0);
let mut s = fresh_scratch();
s.gpz = [800, 500];
s.gdz = [10, 20];
let p = grouscan_run(&mut s, &dummy_inputs(), 0, 0, 1, 1);
assert_eq!(p.lane, 1);
}
#[test]
fn prologue_advances_winning_lane_by_gdz() {
let mut s = fresh_scratch();
s.gpz = [1_000, 2_000];
s.gdz = [256, 999];
let _ = grouscan_run(&mut s, &dummy_inputs(), 0, 0, 1, 1);
assert_eq!(s.gpz[0], 1_256);
assert_eq!(s.gpz[1], 2_000);
}
#[test]
fn prologue_ngxmax_clamps_to_gxmip_when_multimip() {
let mut s = fresh_scratch();
s.gpz = [1_000, 2_000];
s.gdz = [10, 20];
s.gxmax = 1_000_000;
let p = grouscan_run(&mut s, &dummy_inputs(), 0, 0, 50_000, 2);
assert_eq!(p.ngxmax, 50_000);
let mut s = fresh_scratch();
s.gpz = [1_000, 2_000];
s.gdz = [10, 20];
s.gxmax = 1_000_000;
let p = grouscan_run(&mut s, &dummy_inputs(), 0, 0, 50_000, 1);
assert_eq!(p.ngxmax, 1_000_000);
}
#[test]
fn shade_zero_csub_passes_voxel_through_unchanged_intensity() {
let mut tail: u32 = 0x8011_2233;
let _ = grouscan_shade(0x80aa_bbcc, &mut tail, 0);
assert_ne!(tail, 0x8011_2233);
}
#[test]
fn shade_max_csub_produces_zero_intensity_blackout() {
let mut tail: u32 = 0xdead_beef;
let out = grouscan_shade(0xffff_ffff, &mut tail, !0_i64);
assert_eq!(out, 0);
assert_eq!(tail, 0);
}
#[test]
fn cross_sign_basic_signs() {
assert_eq!(grouscan_cross_sign(1 << 16, 1 << 16, 1 << 16, 1), 2);
}
#[test]
fn cross_sign_negative_high_word_uses_signed_extension() {
assert_eq!(grouscan_cross_sign(-(1 << 16), 0, 0, 1), -1);
}
#[test]
fn cross_sign_drops_low_16_of_cx_cy() {
let r1 = grouscan_cross_sign(0x0003_0000, 0, 1 << 16, 1);
let r2 = grouscan_cross_sign(0x0003_FFFF, 0, 1 << 16, 1);
assert_eq!(r1, r2);
}
fn state_for_drawfwall<'a>(
scratch: &'a mut ScanScratch,
column: &'a [u8],
gylookup: &'a [i32],
gcsub: &'a [i64; 9],
) -> GrouscanState<'a> {
let inputs = GrouscanInputs {
column,
gylookup,
gcsub,
slab_buf: &DUMMY_SLAB_BUF,
column_offsets: &DUMMY_COLUMN_OFFSETS,
mip_base_offsets: &DUMMY_MIP_OFFSETS,
vsid: 64,
sky: None,
};
GrouscanState::from_seed(scratch, &inputs, 0, 0, 1)
}
fn state_for_drawcwall<'a>(
scratch: &'a mut ScanScratch,
column: &'a [u8],
gylookup: &'a [i32],
gcsub: &'a [i64; 9],
vptr_offset: usize,
) -> GrouscanState<'a> {
let inputs = GrouscanInputs {
column,
gylookup,
gcsub,
slab_buf: &DUMMY_SLAB_BUF,
column_offsets: &DUMMY_COLUMN_OFFSETS,
mip_base_offsets: &DUMMY_MIP_OFFSETS,
vsid: 64,
sky: None,
};
GrouscanState::from_seed(scratch, &inputs, vptr_offset, 0, 1)
}
#[test]
fn drawfwall_early_exit_when_v1_above_z1() {
let mut s = fresh_scratch();
s.cf[CF_SEED_INDEX].z1 = 30;
s.cf[CF_SEED_INDEX].i0 = 0;
s.cf[CF_SEED_INDEX].i1 = 100;
let column = [0u8, 50, 51, 0]; let gylookup = [0i32; 64];
let gcsub = [0i64; 9];
let mut state = state_for_drawfwall(&mut s, &column, &gylookup, &gcsub);
assert_eq!(phase_draw_fwall(&mut state), Phase::DrawCwall);
assert_eq!(state.ebx, 0);
}
#[test]
fn drawfwall_iterates_until_z1_hits_v1() {
let mut s = fresh_scratch();
s.cf[CF_SEED_INDEX].z1 = 13;
s.cf[CF_SEED_INDEX].i0 = 0;
s.cf[CF_SEED_INDEX].i1 = 100;
s.cf[CF_SEED_INDEX].cx1 = 0;
s.cf[CF_SEED_INDEX].cy1 = 0;
let mut column = vec![0u8, 10, 12, 0];
column.extend_from_slice(&[
0xaa, 0xbb, 0xcc, 0x80, 0x11, 0x22, 0x33, 0x80, 0x44, 0x55, 0x66, 0x80,
]);
let gylookup = [0i32; 64];
let gcsub = [0i64; 9];
let mut state = state_for_drawfwall(&mut s, &column, &gylookup, &gcsub);
assert_eq!(phase_draw_fwall(&mut state), Phase::DrawCwall);
assert_eq!(state.z1, 10);
assert_eq!(state.ebx, 100);
assert_eq!(state.scratch.cf[CF_SEED_INDEX].i1, 100);
}
#[test]
fn drawfwall_writes_pixel_when_cross_sign_positive() {
let mut s = fresh_scratch();
s.cf[CF_SEED_INDEX].z1 = 11;
s.cf[CF_SEED_INDEX].i0 = 0;
s.cf[CF_SEED_INDEX].i1 = 50;
s.cf[CF_SEED_INDEX].cx1 = 1 << 16;
s.cf[CF_SEED_INDEX].cy1 = 0;
s.gi0 = 1 << 16;
s.gi1 = 0;
let mut column = vec![0u8, 10, 11, 0];
column.extend_from_slice(&[0x00, 0x00, 0xff, 0x80]);
let mut gylookup = [0i32; 64];
gylookup[10] = 100;
let gcsub = [0i64; 9];
let mut state = state_for_drawfwall(&mut s, &column, &gylookup, &gcsub);
state.ogx = 0;
let next = phase_draw_fwall(&mut state);
assert_eq!(next, Phase::DrawCwall);
assert_ne!(state.scratch.radar[50].col, 0);
assert_eq!(state.ebx, 49);
}
#[test]
fn drawcwall_column_top_jumps_to_predrawflor() {
let mut s = fresh_scratch();
let column = [0u8, 10, 12, 0]; let gylookup = [0i32; 64];
let gcsub = [0i64; 9];
let mut state = state_for_drawcwall(&mut s, &column, &gylookup, &gcsub, 0);
assert_eq!(phase_draw_cwall(&mut state), Phase::PreDrawFlor);
assert_eq!(state.z1, 10);
}
#[test]
fn drawcwall_dv3_le_z0_jumps_to_predrawceil() {
let mut s = fresh_scratch();
s.cf[CF_SEED_INDEX].z0 = 20;
let mut column = vec![0u8; 32];
column.extend_from_slice(&[0, 10, 12, 5]);
let gylookup = [0i32; 64];
let gcsub = [0i64; 9];
let mut state = state_for_drawcwall(&mut s, &column, &gylookup, &gcsub, 32);
assert_eq!(phase_draw_cwall(&mut state), Phase::PreDrawCeil);
assert_eq!(state.z0, 5);
}
#[test]
fn drawcwall_inner_loop_reads_previous_slab_tail() {
let mut s = fresh_scratch();
s.cf[CF_SEED_INDEX].z0 = 0;
s.cf[CF_SEED_INDEX].i0 = 0;
s.cf[CF_SEED_INDEX].i1 = 100;
s.cf[CF_SEED_INDEX].cx0 = 1 << 16;
s.cf[CF_SEED_INDEX].cy0 = 0;
let mut column = vec![0u8; 16];
column.extend_from_slice(&[0, 10, 12, 2]);
let mut gylookup = [0i32; 64];
gylookup[1] = 1;
gylookup[2] = 1;
let gcsub = [0i64; 9];
let mut state = state_for_drawcwall(&mut s, &column, &gylookup, &gcsub, 16);
state.ogx = 0;
assert_eq!(phase_draw_cwall(&mut state), Phase::PreDrawCeil);
assert_eq!(state.z0, 2);
}
fn state_for_drawceil<'a>(
scratch: &'a mut ScanScratch,
column: &'a [u8],
gylookup: &'a [i32],
gcsub: &'a [i64; 9],
vptr_offset: usize,
) -> GrouscanState<'a> {
let inputs = GrouscanInputs {
column,
gylookup,
gcsub,
slab_buf: &DUMMY_SLAB_BUF,
column_offsets: &DUMMY_COLUMN_OFFSETS,
mip_base_offsets: &DUMMY_MIP_OFFSETS,
vsid: 64,
sky: None,
};
GrouscanState::from_seed(scratch, &inputs, vptr_offset, 0, 1)
}
#[test]
fn predrawceil_swaps_ogx_and_gx() {
let mut s = fresh_scratch();
let inputs = dummy_inputs();
let mut state = GrouscanState::from_seed(&mut s, &inputs, 0, 0, 1);
state.ogx = 0x1111;
state.gx = 0x2222;
assert_eq!(phase_pre_draw_ceil(&mut state), Phase::DrawCeil);
assert_eq!(state.ogx, 0x2222);
assert_eq!(state.gx, 0x1111);
}
#[test]
fn drawceil_cross_sign_positive_jumps_to_drawflor() {
let mut s = fresh_scratch();
s.cf[CF_SEED_INDEX].z0 = 5;
s.cf[CF_SEED_INDEX].cx0 = 1 << 16;
s.cf[CF_SEED_INDEX].cy0 = 0;
s.cf[CF_SEED_INDEX].i0 = 10;
s.cf[CF_SEED_INDEX].i1 = 20;
let mut column = vec![0u8; 8];
column[0..4].copy_from_slice(&[0x44, 0x55, 0x66, 0x80]); let mut gylookup = [0i32; 64];
gylookup[5] = 1;
let gcsub = [0i64; 9];
let mut state = state_for_drawceil(&mut s, &column, &gylookup, &gcsub, 4);
state.ogx = 0;
assert_eq!(phase_draw_ceil(&mut state), Phase::DrawFlor);
assert_eq!(state.scratch.cf[CF_SEED_INDEX].i0, 10);
}
#[test]
fn drawceil_writes_pixel_then_exhausts_radar() {
let mut s = fresh_scratch();
s.cf[CF_SEED_INDEX].z0 = 5;
s.cf[CF_SEED_INDEX].cx0 = 0;
s.cf[CF_SEED_INDEX].cy0 = 0;
s.cf[CF_SEED_INDEX].i0 = 20;
s.cf[CF_SEED_INDEX].i1 = 20;
s.gi0 = 0;
s.gi1 = 0;
let mut column = vec![0u8; 8];
column[0..4].copy_from_slice(&[0x00, 0x00, 0xff, 0x80]);
let mut gylookup = [0i32; 64];
gylookup[5] = 0;
let gcsub = [0i64; 9];
let mut state = state_for_drawceil(&mut s, &column, &gylookup, &gcsub, 4);
state.ogx = 0;
assert_eq!(phase_draw_ceil(&mut state), Phase::DeleteZ);
assert_ne!(state.scratch.radar[20].col, 0);
assert_eq!(state.scratch.cf[CF_SEED_INDEX].i0, 21);
}
#[test]
fn drawceil_bails_when_z0_out_of_gylookup() {
let mut s = fresh_scratch();
s.cf[CF_SEED_INDEX].z0 = 64;
let column = vec![0u8; 8];
let gylookup = [0i32; 64];
let gcsub = [0i64; 9];
let mut state = state_for_drawceil(&mut s, &column, &gylookup, &gcsub, 4);
assert_eq!(phase_draw_ceil(&mut state), Phase::PreDeleteZ);
}
#[test]
fn predrawflor_swaps_ogx_and_gx() {
let mut s = fresh_scratch();
let inputs = dummy_inputs();
let mut state = GrouscanState::from_seed(&mut s, &inputs, 0, 0, 1);
state.ogx = 0x3333;
state.gx = 0x4444;
assert_eq!(phase_pre_draw_flor(&mut state), Phase::DrawFlor);
assert_eq!(state.ogx, 0x4444);
assert_eq!(state.gx, 0x3333);
}
#[test]
fn drawflor_cross_sign_non_positive_returns_after_delete() {
let mut s = fresh_scratch();
s.cf[CF_SEED_INDEX].z1 = 5;
s.cf[CF_SEED_INDEX].cx1 = 0;
s.cf[CF_SEED_INDEX].cy1 = 0;
s.cf[CF_SEED_INDEX].i0 = 10;
s.cf[CF_SEED_INDEX].i1 = 20;
let mut column = vec![0u8; 8];
column[4..8].copy_from_slice(&[0x77, 0x88, 0x99, 0x80]);
let gylookup = [0i32; 64];
let gcsub = [0i64; 9];
let mut state = state_for_drawceil(&mut s, &column, &gylookup, &gcsub, 0);
state.ogx = 0;
assert_eq!(phase_draw_flor(&mut state), Phase::AfterDelete);
assert_eq!(state.scratch.cf[CF_SEED_INDEX].i1, 20);
}
#[test]
fn drawflor_writes_pixel_then_exhausts_radar() {
let mut s = fresh_scratch();
s.cf[CF_SEED_INDEX].z1 = 5;
s.cf[CF_SEED_INDEX].cx1 = 1 << 16;
s.cf[CF_SEED_INDEX].cy1 = 0;
s.cf[CF_SEED_INDEX].i0 = 20;
s.cf[CF_SEED_INDEX].i1 = 20;
s.gi0 = 0;
s.gi1 = 0;
let mut column = vec![0u8; 8];
column[4..8].copy_from_slice(&[0x00, 0x00, 0xff, 0x80]);
let mut gylookup = [0i32; 64];
gylookup[5] = 1;
let gcsub = [0i64; 9];
let mut state = state_for_drawceil(&mut s, &column, &gylookup, &gcsub, 0);
state.ogx = 0;
assert_eq!(phase_draw_flor(&mut state), Phase::DeleteZ);
assert_ne!(state.scratch.radar[20].col, 0);
assert_eq!(state.scratch.cf[CF_SEED_INDEX].i1, 19);
}
#[test]
fn drawflor_bails_when_z1_out_of_gylookup() {
let mut s = fresh_scratch();
s.cf[CF_SEED_INDEX].z1 = 64;
let column = vec![0u8; 8];
let gylookup = [0i32; 64];
let gcsub = [0i64; 9];
let mut state = state_for_drawceil(&mut s, &column, &gylookup, &gcsub, 0);
assert_eq!(phase_draw_flor(&mut state), Phase::PreDeleteZ);
}
#[test]
fn predeletez_swaps_ogx_and_gx() {
let mut s = fresh_scratch();
let inputs = dummy_inputs();
let mut state = GrouscanState::from_seed(&mut s, &inputs, 0, 0, 1);
state.ogx = 0xAAAA;
state.gx = 0xBBBB;
assert_eq!(phase_pre_delete_z(&mut state), Phase::DeleteZ);
assert_eq!(state.ogx, 0xBBBB);
assert_eq!(state.gx, 0xAAAA);
}
#[test]
fn deletez_at_seed_slot_returns_done() {
let mut s = fresh_scratch();
let inputs = dummy_inputs();
let mut state = GrouscanState::from_seed(&mut s, &inputs, 0, 0, 1);
assert_eq!(state.ce_idx, CF_SEED_INDEX);
assert_eq!(phase_delete_z(&mut state), Phase::Done);
}
#[test]
fn deletez_pops_top_when_c_equals_ce() {
let mut s = fresh_scratch();
let inputs = dummy_inputs();
let mut state = GrouscanState::from_seed(&mut s, &inputs, 0, 0, 1);
state.ce_idx = CF_SEED_INDEX + 2;
state.c_idx = CF_SEED_INDEX + 2;
assert_eq!(phase_delete_z(&mut state), Phase::AfterDelete);
assert_eq!(state.ce_idx, CF_SEED_INDEX + 1);
assert_eq!(state.c_idx, CF_SEED_INDEX + 2);
assert_eq!(state.c_presync_idx, usize::MAX);
}
#[test]
fn deletez_shifts_down_when_c_below_ce() {
let mut s = fresh_scratch();
s.cf[CF_SEED_INDEX + 1] = CfType {
i0: 1,
i1: 1,
..Default::default()
};
s.cf[CF_SEED_INDEX + 2] = CfType {
i0: 2,
i1: 2,
..Default::default()
};
let inputs = dummy_inputs();
let mut state = GrouscanState::from_seed(&mut s, &inputs, 0, 0, 1);
state.ce_idx = CF_SEED_INDEX + 2;
state.c_idx = CF_SEED_INDEX + 1;
assert_eq!(phase_delete_z(&mut state), Phase::AfterDeleteKeptPresync);
assert_eq!(state.ce_idx, CF_SEED_INDEX + 1);
assert_eq!(state.c_presync_idx, CF_SEED_INDEX + 2);
assert_eq!(state.scratch.cf[CF_SEED_INDEX + 1].i0, 2);
}
#[test]
fn from_seed_initialises_cf_indices_to_seed() {
let mut s = fresh_scratch();
let inputs = dummy_inputs();
let state = GrouscanState::from_seed(&mut s, &inputs, 0, 0, 1);
assert_eq!(state.c_idx, CF_SEED_INDEX);
assert_eq!(state.ce_idx, CF_SEED_INDEX);
assert_eq!(state.c_presync_idx, usize::MAX);
}
#[test]
fn afterdelete_sets_presync_and_routes_to_kept() {
let mut s = fresh_scratch();
let inputs = dummy_inputs();
let mut state = GrouscanState::from_seed(&mut s, &inputs, 0, 0, 1);
state.c_idx = CF_SEED_INDEX + 1;
state.c_presync_idx = usize::MAX;
assert_eq!(
phase_after_delete(&mut state),
Phase::AfterDeleteKeptPresync
);
assert_eq!(state.c_presync_idx, CF_SEED_INDEX + 1);
}
#[test]
fn afterdelete_kept_presync_routes_to_skipixy_when_c_above_seed() {
let mut s = fresh_scratch();
let inputs = dummy_inputs();
let mut state = GrouscanState::from_seed(&mut s, &inputs, 0, 0, 1);
state.c_idx = CF_SEED_INDEX + 1;
assert_eq!(
phase_after_delete_kept_presync(&mut state),
Phase::SkipixyWithPresync
);
assert_eq!(state.c_idx, CF_SEED_INDEX);
}
#[test]
fn afterdelete_kept_presync_below_seed_runs_column_step() {
let mut s = fresh_scratch();
let inputs = dummy_inputs();
let mut state = GrouscanState::from_seed(&mut s, &inputs, 0, 0, 1);
state.c_idx = CF_SEED_INDEX;
assert_eq!(
phase_after_delete_kept_presync(&mut state),
Phase::SyncFromPresync
);
assert_eq!(state.c_idx, CF_SEED_INDEX);
}
fn build_4x4_world() -> (Vec<u8>, Vec<u32>) {
let mut buf = Vec::with_capacity(16 * 4);
for col in 0..16u8 {
buf.extend_from_slice(&[col, 10, 12, 0]);
}
let offsets: Vec<u32> = (0..16u32).map(|c| c * 4).collect();
(buf, offsets)
}
#[test]
fn column_step_advances_ixy_and_reslices_column() {
let (slab_buf, column_offsets) = build_4x4_world();
let gylookup = DUMMY_GYLOOKUP;
let gcsub = DUMMY_GCSUB;
let inputs = GrouscanInputs {
column: &slab_buf[20..], gylookup: &gylookup,
gcsub: &gcsub,
slab_buf: &slab_buf,
column_offsets: &column_offsets,
mip_base_offsets: &[0, column_offsets.len()],
vsid: 4,
sky: None,
};
let mut s = fresh_scratch();
s.gixy = [1, 4]; let mut state = GrouscanState::from_seed(&mut s, &inputs, 0, 5, 1);
state.c_idx = CF_SEED_INDEX; state.lane = 0;
let next = phase_after_delete_kept_presync(&mut state);
assert!(matches!(
next,
Phase::SyncFromPresync | Phase::Skipixy3 | Phase::Remiporend
));
assert_eq!(state.ixy_sptr_col_idx, 6);
assert_eq!(state.column[0], 6);
assert_eq!(state.wall_lane, 0);
}
#[test]
fn column_step_routes_to_remiporend_when_gpz_exceeds_ngxmax() {
let (slab_buf, column_offsets) = build_4x4_world();
let gylookup = DUMMY_GYLOOKUP;
let gcsub = DUMMY_GCSUB;
let inputs = GrouscanInputs {
column: &slab_buf[..],
gylookup: &gylookup,
gcsub: &gcsub,
slab_buf: &slab_buf,
column_offsets: &column_offsets,
mip_base_offsets: &[0, column_offsets.len()],
vsid: 4,
sky: None,
};
let mut s = fresh_scratch();
s.gpz = [0x100, 0x200];
let mut state = GrouscanState::from_seed(&mut s, &inputs, 0, 0, 1);
state.ngxmax = 0xFF;
state.c_idx = CF_SEED_INDEX;
assert_eq!(
phase_after_delete_kept_presync(&mut state),
Phase::Remiporend
);
}
#[test]
fn remiporend_routes_to_startsky_when_no_more_mips() {
let mut s = fresh_scratch();
let inputs = dummy_inputs();
let mut state = GrouscanState::from_seed(&mut s, &inputs, 0, 0, 1);
state.gmipcnt = 0;
assert_eq!(phase_remiporend(&mut state), Phase::Startsky);
}
#[test]
fn remiporend_multimip_falls_through_to_startsky() {
let mut s = fresh_scratch();
let inputs = dummy_inputs();
let mut state = GrouscanState::from_seed(&mut s, &inputs, 0, 0, 4);
state.gmipcnt = 0;
assert_eq!(phase_remiporend(&mut state), Phase::Startsky);
}
#[test]
fn startsky_returns_done_when_stack_below_seed() {
let mut s = fresh_scratch();
let inputs = dummy_inputs();
let mut state = GrouscanState::from_seed(&mut s, &inputs, 0, 0, 1);
state.ce_idx = CF_SEED_INDEX - 1;
assert_eq!(phase_startsky(&mut state), Phase::Done);
}
#[test]
fn startsky_solid_fills_radar_with_skycast() {
let mut s = fresh_scratch();
let sky_col_bits: u32 = 0x80AB_CDEF;
s.set_skycast(sky_col_bits.cast_signed(), 0x7FFF_FFFF);
s.cf[CF_SEED_INDEX].i0 = 10;
s.cf[CF_SEED_INDEX].i1 = 13;
let inputs = dummy_inputs();
let mut state = GrouscanState::from_seed(&mut s, &inputs, 0, 0, 1);
state.ce_idx = CF_SEED_INDEX;
assert_eq!(phase_startsky(&mut state), Phase::Done);
for p in 10usize..=13 {
assert_eq!(state.scratch.radar[p].col, sky_col_bits.cast_signed());
assert_eq!(state.scratch.radar[p].dist, 0x7FFF_FFFF);
}
assert_eq!(state.scratch.radar[9].col, 0);
assert_eq!(state.scratch.radar[14].col, 0);
}
#[test]
fn startsky_walks_multiple_cf_entries() {
let mut s = fresh_scratch();
s.set_skycast(0x1234_5678, 0);
s.cf[CF_SEED_INDEX].i0 = 0;
s.cf[CF_SEED_INDEX].i1 = 1;
s.cf[CF_SEED_INDEX + 1].i0 = 5;
s.cf[CF_SEED_INDEX + 1].i1 = 6;
let inputs = dummy_inputs();
let mut state = GrouscanState::from_seed(&mut s, &inputs, 0, 0, 1);
state.ce_idx = CF_SEED_INDEX + 1;
assert_eq!(phase_startsky(&mut state), Phase::Done);
assert_eq!(state.scratch.radar[0].col, 0x1234_5678);
assert_eq!(state.scratch.radar[1].col, 0x1234_5678);
assert_eq!(state.scratch.radar[2].col, 0);
assert_eq!(state.scratch.radar[5].col, 0x1234_5678);
assert_eq!(state.scratch.radar[6].col, 0x1234_5678);
}
#[test]
fn column_step_routes_to_skipixy3_when_presync_equals_c() {
let (slab_buf, column_offsets) = build_4x4_world();
let gylookup = DUMMY_GYLOOKUP;
let gcsub = DUMMY_GCSUB;
let inputs = GrouscanInputs {
column: &slab_buf[..],
gylookup: &gylookup,
gcsub: &gcsub,
slab_buf: &slab_buf,
column_offsets: &column_offsets,
mip_base_offsets: &[0, column_offsets.len()],
vsid: 4,
sky: None,
};
let mut s = fresh_scratch();
let mut state = GrouscanState::from_seed(&mut s, &inputs, 0, 0, 1);
state.c_idx = CF_SEED_INDEX;
state.ce_idx = CF_SEED_INDEX;
state.c_presync_idx = CF_SEED_INDEX;
assert_eq!(phase_after_delete_kept_presync(&mut state), Phase::Skipixy3);
}
#[test]
fn column_step_resets_vptr_offset_to_zero() {
let (slab_buf, column_offsets) = build_4x4_world();
let gylookup = DUMMY_GYLOOKUP;
let gcsub = DUMMY_GCSUB;
let inputs = GrouscanInputs {
column: &slab_buf[..],
gylookup: &gylookup,
gcsub: &gcsub,
slab_buf: &slab_buf,
column_offsets: &column_offsets,
mip_base_offsets: &[0, column_offsets.len()],
vsid: 4,
sky: None,
};
let mut s = fresh_scratch();
let mut state = GrouscanState::from_seed(&mut s, &inputs, 32, 0, 1);
state.c_idx = CF_SEED_INDEX;
let _ = phase_after_delete_kept_presync(&mut state);
assert_eq!(state.vptr_offset, 0);
}
#[test]
fn skipixy3_routes_to_drawfwall_when_v0_zero() {
let mut s = fresh_scratch();
let column = [0u8, 10, 12, 0]; let gylookup = [0i32; 64];
let gcsub = [0i64; 9];
let mut state = state_for_drawcwall(&mut s, &column, &gylookup, &gcsub, 0);
assert_eq!(phase_skipixy3(&mut state), Phase::DrawFwall);
}
#[test]
fn skipixy3_routes_to_intoslabloop_when_v0_nonzero() {
let mut s = fresh_scratch();
let column = [2u8, 10, 12, 0, 0, 0, 0, 0, 0, 20, 22, 0];
let gylookup = [0i32; 64];
let gcsub = [0i64; 9];
let mut state = state_for_drawcwall(&mut s, &column, &gylookup, &gcsub, 0);
assert_eq!(phase_skipixy3(&mut state), Phase::Intoslabloop);
}
#[test]
fn intoslabloop_routes_to_findslabloop_when_test_hi_positive() {
let mut s = fresh_scratch();
s.cf[CF_SEED_INDEX].cx0 = 1 << 16;
let column = [2u8, 10, 12, 0, 0, 0, 0, 0, 0, 20, 22, 0];
let mut gylookup = [0i32; 64];
gylookup[13] = 1; let gcsub = [0i64; 9];
let mut state = state_for_drawcwall(&mut s, &column, &gylookup, &gcsub, 0);
state.ogx = 0;
assert_eq!(phase_intoslabloop(&mut state), Phase::Findslabloop);
}
#[test]
fn intoslabloop_routes_to_drawfwall_when_test_hi_and_test_next_nonpositive() {
let mut s = fresh_scratch();
let column = [2u8, 10, 12, 0, 0, 0, 0, 0, 0, 20, 22, 0];
let gylookup = [0i32; 64];
let gcsub = [0i64; 9];
let mut state = state_for_drawcwall(&mut s, &column, &gylookup, &gcsub, 0);
state.ogx = 0;
assert_eq!(phase_intoslabloop(&mut state), Phase::DrawFwall);
}
#[test]
fn intoslabloop_pushes_split_when_test_next_positive() {
let mut s = fresh_scratch();
s.cf[CF_SEED_INDEX].i0 = 0;
s.cf[CF_SEED_INDEX].i1 = 5;
s.cf[CF_SEED_INDEX].cx1 = 1 << 16;
s.cf[CF_SEED_INDEX].cy1 = 0;
s.gi0 = 0;
s.gi1 = 0;
let column = [
2u8, 10, 12, 0, 0, 0, 0, 0, 0u8, 20, 22, 5, ];
let mut gylookup = [0i32; 64];
gylookup[5] = 1; let gcsub = [0i64; 9];
let mut state = state_for_drawcwall(&mut s, &column, &gylookup, &gcsub, 0);
state.cx1 = 1 << 16;
state.cy1 = 0;
state.ogx = 0;
state.z0 = 0;
state.z1 = 99;
assert_eq!(phase_intoslabloop(&mut state), Phase::DrawFwall);
assert_eq!(state.ce_idx, CF_SEED_INDEX + 1);
assert_eq!(state.c_idx, CF_SEED_INDEX + 1);
assert_eq!(state.z1, 5);
assert_eq!(state.z0, 0);
assert_eq!(state.scratch.cf[CF_SEED_INDEX].z0, 5);
assert_eq!(state.scratch.cf[CF_SEED_INDEX].i0, 6);
assert_eq!(state.scratch.cf[CF_SEED_INDEX + 1].i1, 5);
}
#[test]
fn slab_split_returns_done_when_stack_full() {
let mut s = fresh_scratch();
s.cf[CF_SEED_INDEX].i0 = 0;
s.cf[CF_SEED_INDEX].i1 = 5;
s.cf[CF_SEED_INDEX].cx1 = 1 << 16;
let column = [2u8, 10, 12, 0, 0, 0, 0, 0, 0u8, 20, 22, 5];
let mut gylookup = [0i32; 64];
gylookup[5] = 1;
let gcsub = [0i64; 9];
let mut state = state_for_drawcwall(&mut s, &column, &gylookup, &gcsub, 0);
state.cx1 = 1 << 16;
state.ce_idx = 191; state.ogx = 0;
assert_eq!(phase_intoslabloop(&mut state), Phase::Done);
assert_eq!(state.ce_idx, 191);
}
#[test]
fn findslabloop_advances_vptr_then_intoslabloop_when_next_nonzero() {
let mut s = fresh_scratch();
let column = [
2u8, 10, 12, 0, 0, 0, 0, 0, 2u8, 20, 22, 0, 0, 0, 0, 0, 0u8, 30, 32, 0, ];
let gylookup = [0i32; 64];
let gcsub = [0i64; 9];
let mut state = state_for_drawcwall(&mut s, &column, &gylookup, &gcsub, 0);
assert_eq!(phase_findslabloop(&mut state), Phase::Intoslabloop);
assert_eq!(state.vptr_offset, 8);
}
#[test]
fn findslabloop_routes_to_drawfwall_when_next_v0_zero() {
let mut s = fresh_scratch();
let column = [1u8, 10, 12, 0, 0u8, 20, 22, 0];
let gylookup = [0i32; 64];
let gcsub = [0i64; 9];
let mut state = state_for_drawcwall(&mut s, &column, &gylookup, &gcsub, 0);
assert_eq!(phase_findslabloop(&mut state), Phase::DrawFwall);
assert_eq!(state.vptr_offset, 4);
}
#[test]
fn skipixy_with_presync_swaps_ogx_and_routes_to_sync() {
let mut s = fresh_scratch();
let inputs = dummy_inputs();
let mut state = GrouscanState::from_seed(&mut s, &inputs, 0, 0, 1);
state.ogx = 0xAAAA;
state.gx = 0xBBBB;
assert_eq!(
phase_skipixy_with_presync(&mut state),
Phase::SyncFromPresync
);
assert_eq!(state.ogx, 0xBBBB);
assert_eq!(state.gx, 0xAAAA);
}
#[test]
fn sync_from_presync_saves_to_presync_and_loads_from_c() {
let mut s = fresh_scratch();
s.cf[CF_SEED_INDEX + 1] = CfType {
i0: 0,
i1: 0,
z0: 100,
z1: 200,
cx0: 300,
cy0: 400,
cx1: 500,
cy1: 600,
};
let inputs = dummy_inputs();
let mut state = GrouscanState::from_seed(&mut s, &inputs, 0, 0, 1);
state.z0 = 1;
state.z1 = 2;
state.cx0 = 3;
state.cy0 = 4;
state.cx1 = 5;
state.cy1 = 6;
state.ogx = 0xAAAA;
state.gx = 0xBBBB;
state.c_idx = CF_SEED_INDEX + 1;
state.c_presync_idx = CF_SEED_INDEX + 2;
assert_eq!(phase_sync_from_presync(&mut state), Phase::Skipixy3);
assert_eq!(state.ogx, 0xAAAA);
assert_eq!(state.gx, 0xBBBB);
let presync = state.scratch.cf[CF_SEED_INDEX + 2];
assert_eq!(presync.z0, 1);
assert_eq!(presync.z1, 2);
assert_eq!(presync.cx0, 3);
assert_eq!(presync.cy0, 4);
assert_eq!(presync.cx1, 5);
assert_eq!(presync.cy1, 6);
assert_eq!(state.z0, 100);
assert_eq!(state.z1, 200);
assert_eq!(state.cx0, 300);
assert_eq!(state.cy0, 400);
assert_eq!(state.cx1, 500);
assert_eq!(state.cy1, 600);
}
#[test]
fn from_seed_carries_ixy_sptr_col_idx() {
let mut s = fresh_scratch();
let inputs = dummy_inputs();
let state = GrouscanState::from_seed(&mut s, &inputs, 0, 42, 1);
assert_eq!(state.ixy_sptr_col_idx, 42);
}
#[test]
fn dispatch_drawflor_when_camera_at_top_of_column() {
let mut s = fresh_scratch();
s.gpz = [1_000, 2_000];
s.gdz = [10, 20];
let p = grouscan_run(&mut s, &dummy_inputs(), 0, 0, 1, 1);
assert_eq!(p.dispatch, InitialDispatch::DrawFlor);
}
#[test]
fn dispatch_drawceil_when_camera_in_interior_air_gap() {
let mut s = fresh_scratch();
s.gpz = [1_000, 2_000];
s.gdz = [10, 20];
let p = grouscan_run(&mut s, &dummy_inputs(), 16, 0, 1, 1);
assert_eq!(p.dispatch, InitialDispatch::DrawCeil);
}
#[test]
fn prologue_ogx_keeps_integer_part_of_gpz() {
let mut s = fresh_scratch();
s.gpz = [0x1234_5678, 0x7FFF_FFFF];
s.gdz = [0, 0];
let p = grouscan_run(&mut s, &dummy_inputs(), 0, 0, 1, 1);
assert_eq!(p.lane, 0);
assert_eq!(p.ogx, 0x1234_0000_i32);
assert_eq!(p.gx, 0);
}
}