#![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 chz_layer: i32,
}
pub const CF_LEN: usize = 256;
pub const CF_SEED_INDEX: usize = 128;
use std::ops::Deref;
use crate::rasterizer::ScanScratch;
use crate::sky::Sky;
#[derive(Debug)]
pub enum ColumnSource<'a> {
Borrowed(&'a [u8]),
Owned(Vec<u8>),
}
impl<'a> ColumnSource<'a> {
#[must_use]
pub fn new_owned() -> Self {
Self::Owned(Vec::new())
}
#[must_use]
pub fn with_owned_capacity(cap: usize) -> Self {
Self::Owned(Vec::with_capacity(cap))
}
#[inline]
pub fn clear(&mut self) {
match self {
Self::Borrowed(b) => *b = &[],
Self::Owned(v) => v.clear(),
}
}
#[inline]
pub fn set_borrowed(&mut self, slab_buf: &'a [u8], off: usize, len: usize) {
let end = off.saturating_add(len).min(slab_buf.len());
*self = Self::Borrowed(&slab_buf[off..end]);
}
#[inline]
pub fn set_borrowed_slice(&mut self, bytes: &'a [u8]) {
*self = Self::Borrowed(bytes);
}
#[inline]
pub fn as_owned_mut(&mut self) -> &mut Vec<u8> {
if !matches!(self, Self::Owned(_)) {
*self = Self::Owned(Vec::new());
}
match self {
Self::Owned(v) => v,
Self::Borrowed(_) => unreachable!("just installed Owned variant"),
}
}
}
impl<'a> Deref for ColumnSource<'a> {
type Target = [u8];
#[inline(always)]
fn deref(&self) -> &[u8] {
match self {
Self::Borrowed(b) => b,
Self::Owned(v) => v.as_slice(),
}
}
}
impl Default for ColumnSource<'_> {
fn default() -> Self {
Self::new_owned()
}
}
static TRACE_PHASES: std::sync::LazyLock<bool> =
std::sync::LazyLock::new(|| std::env::var("ROXLAP_TRACE_PHASES").is_ok());
static TRACE_STARTSKY: std::sync::LazyLock<bool> =
std::sync::LazyLock::new(|| std::env::var("ROXLAP_TRACE_STARTSKY").is_ok());
#[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>>,
pub grid_view: crate::grid_view::GridView<'a>,
pub camera_chunk_z: i32,
pub chunk_world_z_base: i32,
pub chunk_size_z: u32,
}
#[allow(dead_code)]
pub(crate) struct GrouscanState<'a> {
pub scratch: &'a mut ScanScratch,
pub column: ColumnSource<'a>,
pub column_z_base: Vec<i32>,
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 cx: i32,
pub cy: i32,
pub cx_mip: i32,
pub cy_mip: i32,
pub vsid_signed: i32,
pub gmipcnt: i32,
pub gmipnum: u32,
pub grid_view: crate::grid_view::GridView<'a>,
pub chunk_size_xy: u32,
pub chunk_size_xy_log2: u32,
pub chunk_size_xy_mask: i32,
pub current_chunk_idx_xy: [i32; 2],
pub current_chunk_exists: bool,
pub camera_chunk_z: i32,
pub chunk_world_z_base: i32,
pub chunk_size_z: u32,
pub current_chunk_z: i32,
pub starting_chz: i32,
pub max_chz: i32,
}
impl<'a> GrouscanState<'a> {
#[allow(clippy::too_many_arguments)]
fn from_seed(
scratch: &'a mut ScanScratch,
inputs: &GrouscanInputs<'a>,
vptr_offset: usize,
ixy_sptr_col_idx: usize,
cx: i32,
cy: i32,
gmipnum: u32,
) -> Self {
let c = scratch.cf[CF_SEED_INDEX];
let grid_view = inputs.grid_view;
let chunk_size_xy = grid_view.chunk_size_xy;
debug_assert!(
chunk_size_xy.is_power_of_two() && chunk_size_xy > 0,
"chunk_size_xy must be a positive power of two (got {chunk_size_xy})"
);
let chunk_size_xy_log2 = chunk_size_xy.trailing_zeros();
#[allow(clippy::cast_possible_wrap)]
let chunk_size_xy_mask = (chunk_size_xy - 1) as i32;
let current_chunk_idx_xy = [cx >> chunk_size_xy_log2, cy >> chunk_size_xy_log2];
let camera_chunk_z = inputs.camera_chunk_z;
let current_chunk_exists = grid_view
.chunk_at_xyz([
current_chunk_idx_xy[0],
current_chunk_idx_xy[1],
camera_chunk_z,
])
.is_some();
let mut column: ColumnSource<'a> =
ColumnSource::with_owned_capacity(inputs.column.len().min(8192));
let mut column_z_base_owned: Vec<i32> = Vec::with_capacity(inputs.column.len().min(8192));
if let Some(cg) = grid_view.chunk_grid {
#[allow(clippy::cast_possible_wrap)]
let chunks_z_signed = cg.chunks_z as i32;
let max_chz = cg.origin_chunk_z + chunks_z_signed - 1;
let starting_chz = inputs.camera_chunk_z.clamp(cg.origin_chunk_z, max_chz);
let chunk_local_xy = [cx & chunk_size_xy_mask, cy & chunk_size_xy_mask];
#[allow(clippy::cast_possible_wrap)]
let chunk_size_z_signed = inputs.chunk_size_z as i32;
build_owned_column_multi_chz(
&mut column,
&mut column_z_base_owned,
grid_view,
current_chunk_idx_xy,
chunk_local_xy,
starting_chz,
max_chz,
chunk_size_z_signed,
0,
);
} else {
let seed_chain_len = slab_chain_byte_len(inputs.column);
column.set_borrowed(inputs.column, 0, seed_chain_len);
}
Self {
scratch,
column,
column_z_base: column_z_base_owned,
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,
cx,
cy,
cx_mip: cx,
cy_mip: cy,
#[allow(clippy::cast_possible_wrap)]
vsid_signed: inputs.vsid as i32,
gmipcnt: 0,
gmipnum,
grid_view,
chunk_size_xy,
chunk_size_xy_log2,
chunk_size_xy_mask,
current_chunk_idx_xy,
current_chunk_exists,
camera_chunk_z,
chunk_world_z_base: inputs.chunk_world_z_base,
chunk_size_z: inputs.chunk_size_z,
current_chunk_z: camera_chunk_z,
starting_chz: {
#[allow(clippy::cast_possible_wrap)]
let (origin, max) = grid_view.chunk_grid.map_or((0, 0), |cg| {
(
cg.origin_chunk_z,
cg.origin_chunk_z + cg.chunks_z as i32 - 1,
)
});
camera_chunk_z.clamp(origin, max)
},
max_chz: {
#[allow(clippy::cast_possible_wrap)]
grid_view
.chunk_grid
.map_or(0, |cg| cg.origin_chunk_z + cg.chunks_z as i32 - 1)
},
}
}
}
#[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,
}
#[allow(clippy::too_many_arguments)]
#[must_use]
pub fn grouscan_run(
scratch: &mut ScanScratch,
inputs: &GrouscanInputs<'_>,
vptr_offset: usize,
ixy_sptr_col_idx: usize,
cx: i32,
cy: i32,
gxmip: i32,
gmipnum: u32,
) -> GrouscanPrologue {
let mut state = GrouscanState::from_seed(
scratch,
inputs,
vptr_offset,
ixy_sptr_col_idx,
cx,
cy,
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 = *TRACE_PHASES;
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 = slab_z_at(state, state.vptr_offset, 1);
let bedrock_z_at_mip = 0xff_u8 >> (state.gmipcnt as u32);
if state.scratch.treat_z_max_as_air
&& state.vptr_offset + 1 < state.column.len()
&& state.column[state.vptr_offset + 1] == bedrock_z_at_mip
{
return Phase::DrawCwall;
}
if dv1 >= state.z1 {
return Phase::DrawCwall;
}
state.ebx = state.scratch.cf[state.c_idx].i1;
'outer: loop {
state.off = state.z1 - slab_z_at(state, 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;
state.scratch.cf[state.c_idx].cx1 = state.cx1;
state.scratch.cf[state.c_idx].cy1 = state.cy1;
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;
state.scratch.cf[state.c_idx].cx1 = state.cx1;
state.scratch.cf[state.c_idx].cy1 = state.cy1;
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 slab_z_at(state, state.vptr_offset, 1) != state.z1 {
continue 'outer;
}
state.scratch.cf[state.c_idx].i1 = state.ebx;
state.scratch.cf[state.c_idx].cx1 = state.cx1;
state.scratch.cf[state.c_idx].cy1 = state.cy1;
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 = slab_z_at(state, state.vptr_offset, 1);
if state.vptr_offset == 0 {
return Phase::PreDrawFlor;
}
let dv3 = slab_z_at(state, 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 - slab_z_at(state, 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.scratch.cf[state.c_idx].cx0 = state.cx0;
state.scratch.cf[state.c_idx].cy0 = state.cy0;
state.z0 = slab_z_at(state, 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.scratch.cf[state.c_idx].cx0 = state.cx0;
state.scratch.cf[state.c_idx].cy0 = state.cy0;
state.z0 = slab_z_at(state, 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 slab_z_at(state, state.vptr_offset, 3) != state.z0 {
continue 'outer;
}
state.scratch.cf[state.c_idx].i0 = state.ebx;
state.scratch.cf[state.c_idx].cx0 = state.cx0;
state.scratch.cf[state.c_idx].cy0 = state.cy0;
state.z0 = slab_z_at(state, 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::AfterDelete;
}
state.gy_raw = state.gylookup[z0_idx];
if state.vptr_offset < 4 {
return Phase::AfterDelete;
}
let vox_off = state.vptr_offset - 4;
if vox_off + 4 > state.column.len() {
return Phase::AfterDelete;
}
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 {
state.scratch.cf[state.c_idx].cx0 = state.cx0;
state.scratch.cf[state.c_idx].cy0 = state.cy0;
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 {
state.scratch.cf[state.c_idx].cx0 = state.cx0;
state.scratch.cf[state.c_idx].cy0 = state.cy0;
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 bedrock_z_at_mip = 0xff_u8 >> (state.gmipcnt as u32);
if state.scratch.treat_z_max_as_air
&& state.vptr_offset + 1 < state.column.len()
&& state.column[state.vptr_offset + 1] == bedrock_z_at_mip
{
return Phase::AfterDelete;
}
let z1_idx = state.z1 as usize;
if z1_idx >= state.gylookup.len() {
return Phase::AfterDelete;
}
state.gy_raw = state.gylookup[z1_idx];
let vox_off = state.vptr_offset + 4;
if vox_off + 4 > state.column.len() {
return Phase::AfterDelete;
}
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 {
state.scratch.cf[state.c_idx].cx1 = state.cx1;
state.scratch.cf[state.c_idx].cy1 = state.cy1;
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 {
state.scratch.cf[state.c_idx].cx1 = state.cx1;
state.scratch.cf[state.c_idx].cy1 = state.cy1;
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 state.lane == 0 {
state.cx += state.scratch.gixy[0].signum();
state.cx_mip += state.scratch.gixy[0].signum();
} else {
state.cy += state.scratch.gixy[1].signum();
state.cy_mip += state.scratch.gixy[1].signum();
}
if state.grid_view.chunk_grid.is_none() {
if state.gmipcnt > 0 {
let gmip = state.gmipcnt as u32;
let chunk_size_at_mip = (state.chunk_size_xy >> gmip) as i32;
let mip_base = state.mip_base_offsets[state.gmipcnt as usize];
let in_bounds = state.cx_mip >= 0
&& state.cy_mip >= 0
&& state.cx_mip < chunk_size_at_mip
&& state.cy_mip < chunk_size_at_mip;
if in_bounds {
#[allow(clippy::cast_sign_loss)]
let correct_idx = mip_base
+ ((state.cy_mip as u32) * (chunk_size_at_mip as u32) + (state.cx_mip as u32))
as usize;
state.ixy_sptr_col_idx = correct_idx;
if let Some(&col_off) = state.column_offsets.get(correct_idx) {
let off = col_off as usize;
if off <= state.slab_buf.len() {
let chain_len = slab_chain_byte_len(&state.slab_buf[off..]);
state.column.set_borrowed(state.slab_buf, off, chain_len);
state.column_z_base.clear();
} else {
state.column.clear();
state.column_z_base.clear();
}
} else {
state.column.clear();
state.column_z_base.clear();
}
} else {
state.column.clear();
state.column_z_base.clear();
}
} else {
let in_bounds = state.cx >= 0
&& state.cy >= 0
&& state.cx < state.vsid_signed
&& state.cy < state.vsid_signed;
if in_bounds {
#[allow(clippy::cast_sign_loss)]
let correct_idx = (state.cy as u32)
.wrapping_mul(state.vsid)
.wrapping_add(state.cx as u32) as usize;
state.ixy_sptr_col_idx = correct_idx;
if let Some(&col_off) = state.column_offsets.get(correct_idx) {
let off = col_off as usize;
if off <= state.slab_buf.len() {
let chain_len = slab_chain_byte_len(&state.slab_buf[off..]);
state.column.set_borrowed(state.slab_buf, off, chain_len);
state.column_z_base.clear();
}
}
} else {
state.column.clear();
state.column_z_base.clear();
}
}
} else {
if state.gmipcnt > 0 {
let gmip = state.gmipcnt as u32;
let chunk_size_at_mip = (state.chunk_size_xy >> gmip) as i32;
let chunk_size_at_mip_mask = chunk_size_at_mip - 1;
let chunk_size_at_mip_log2 = state.chunk_size_xy_log2 - gmip;
let new_chunk_xy = [
state.cx_mip >> chunk_size_at_mip_log2,
state.cy_mip >> chunk_size_at_mip_log2,
];
if new_chunk_xy != state.current_chunk_idx_xy {
state.current_chunk_idx_xy = new_chunk_xy;
if let Some(new_chunk) = state.grid_view.chunk_at_xyz([
new_chunk_xy[0],
new_chunk_xy[1],
state.current_chunk_z,
]) {
state.slab_buf = new_chunk.slab_buf;
state.column_offsets = new_chunk.column_offsets;
state.mip_base_offsets = new_chunk.mip_base_offsets;
state.vsid = new_chunk.vsid;
#[allow(clippy::cast_possible_wrap)]
{
state.vsid_signed = new_chunk.vsid as i32;
}
state.current_chunk_exists = true;
} else {
state.current_chunk_exists = false;
}
}
let local_cx_mip = state.cx_mip & chunk_size_at_mip_mask;
let local_cy_mip = state.cy_mip & chunk_size_at_mip_mask;
let mip_base = state.mip_base_offsets[state.gmipcnt as usize];
#[allow(clippy::cast_sign_loss)]
let correct_idx = mip_base
+ ((local_cy_mip as u32) * (chunk_size_at_mip as u32) + (local_cx_mip as u32))
as usize;
state.ixy_sptr_col_idx = correct_idx;
#[allow(clippy::cast_possible_wrap)]
let chunk_size_z_signed = state.chunk_size_z as i32;
let chunk_local_xy_mip = [local_cx_mip, local_cy_mip];
build_owned_column_multi_chz(
&mut state.column,
&mut state.column_z_base,
state.grid_view,
new_chunk_xy,
chunk_local_xy_mip,
state.starting_chz,
state.max_chz,
chunk_size_z_signed,
state.gmipcnt as u32,
);
} else {
let log2 = state.chunk_size_xy_log2;
let mask = state.chunk_size_xy_mask;
let new_chunk_xy = [state.cx >> log2, state.cy >> log2];
if new_chunk_xy != state.current_chunk_idx_xy {
state.current_chunk_idx_xy = new_chunk_xy;
if let Some(new_chunk) = state.grid_view.chunk_at_xyz([
new_chunk_xy[0],
new_chunk_xy[1],
state.current_chunk_z,
]) {
state.slab_buf = new_chunk.slab_buf;
state.column_offsets = new_chunk.column_offsets;
state.mip_base_offsets = new_chunk.mip_base_offsets;
state.vsid = new_chunk.vsid;
#[allow(clippy::cast_possible_wrap)]
{
state.vsid_signed = new_chunk.vsid as i32;
}
state.current_chunk_exists = true;
} else {
state.current_chunk_exists = false;
}
}
let local_cx = state.cx & mask;
let local_cy = state.cy & mask;
#[allow(clippy::cast_sign_loss)]
let correct_idx = (local_cy as u32)
.wrapping_mul(state.chunk_size_xy)
.wrapping_add(local_cx as u32) as usize;
state.ixy_sptr_col_idx = correct_idx;
let starting_chz = state.starting_chz;
let max_chz = state.max_chz;
#[allow(clippy::cast_possible_wrap)]
let chunk_size_z_signed = state.chunk_size_z as i32;
if starting_chz == max_chz {
if state.current_chunk_exists {
if let Some(&col_off) = state.column_offsets.get(correct_idx) {
let off = col_off as usize;
if off <= state.slab_buf.len() {
let world_z_base = starting_chz * chunk_size_z_signed;
state.chunk_world_z_base = world_z_base;
let chain_len = slab_chain_byte_len(&state.slab_buf[off..]);
state.column.set_borrowed(state.slab_buf, off, chain_len);
state.column_z_base.clear();
}
}
} else {
state.column.clear();
state.column_z_base.clear();
}
} else {
let chunk_local_xy = [local_cx, local_cy];
build_owned_column_multi_chz(
&mut state.column,
&mut state.column_z_base,
state.grid_view,
new_chunk_xy,
chunk_local_xy,
starting_chz,
max_chz,
chunk_size_z_signed,
0,
);
}
}
}
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 = slab_z_at(state, state.vptr_offset, 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 = slab_z_at(state, state.vptr_offset + (v0 as usize) * 4, 3);
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)
}
fn slab_chain_byte_len(column: &[u8]) -> usize {
let mut pos = 0usize;
loop {
if pos + 4 > column.len() {
return column.len();
}
let nextptr = column[pos];
if nextptr == 0 {
let z1 = i32::from(column[pos + 1]);
let z1c = i32::from(column[pos + 2]);
let n_floor_signed = z1c - z1 + 1;
let n_floor = usize::try_from(n_floor_signed.max(0)).unwrap_or(0);
let last_size = 4 + n_floor * 4;
return (pos + last_size).min(column.len());
}
let advance = usize::from(nextptr) * 4;
if advance < 4 {
return column.len();
}
pos = pos.saturating_add(advance);
}
}
fn install_owned_column(
target: &mut Vec<u8>,
target_z_base: &mut Vec<i32>,
slab_buf: &[u8],
off: usize,
_world_z_base: i32,
) {
target.clear();
build_owned_column_from_chain(target, slab_buf, off);
target_z_base.clear();
}
fn build_owned_column_from_chain(target: &mut Vec<u8>, slab_buf: &[u8], off: usize) {
if off >= slab_buf.len() {
return;
}
let tail = &slab_buf[off..];
let mut pos = 0usize;
loop {
if pos + 4 > tail.len() {
target.extend_from_slice(&tail[pos..]);
return;
}
let nextptr = tail[pos];
if nextptr == 0 {
let z1 = i32::from(tail[pos + 1]);
let z1c = i32::from(tail[pos + 2]);
let n_floor = usize::try_from((z1c - z1 + 1).max(0)).unwrap_or(0);
let last_size = 4 + n_floor * 4;
let end = (pos + last_size).min(tail.len());
target.extend_from_slice(&tail[pos..end]);
return;
}
let advance = usize::from(nextptr) * 4;
if advance < 4 {
let end = (pos + 4).min(tail.len());
target.extend_from_slice(&tail[pos..end]);
return;
}
let next_pos = pos.saturating_add(advance).min(tail.len());
target.extend_from_slice(&tail[pos..next_pos]);
if next_pos >= tail.len() {
return;
}
pos = next_pos;
}
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn build_owned_column_from_chain_translated(
target: &mut Vec<u8>,
slab_buf: &[u8],
off: usize,
world_z_base: i32,
) {
if off >= slab_buf.len() {
return;
}
let tail = &slab_buf[off..];
let mut pos = 0usize;
loop {
if pos + 4 > tail.len() {
target.extend_from_slice(&tail[pos..]);
return;
}
let nextptr = tail[pos];
let z1_raw = i32::from(tail[pos + 1]);
let z1c_raw = i32::from(tail[pos + 2]);
let z0_raw = i32::from(tail[pos + 3]);
let z1_w = z1_raw + world_z_base;
let z1c_w = z1c_raw + world_z_base;
let z0_w = z0_raw + world_z_base;
assert!(
(0..256).contains(&z1_w)
&& (0..256).contains(&z1c_w)
&& (0..256).contains(&z0_w),
"VC.3 z translation overflows u8 (base={world_z_base}, raw z1={z1_raw}, z1c={z1c_raw}, z0={z0_raw})"
);
target.push(nextptr);
target.push(z1_w as u8);
target.push(z1c_w as u8);
target.push(z0_w as u8);
if nextptr == 0 {
let n_floor = usize::try_from((z1c_raw - z1_raw + 1).max(0)).unwrap_or(0);
let body_bytes = n_floor * 4;
let body_end = (pos + 4 + body_bytes).min(tail.len());
if pos + 4 < body_end {
target.extend_from_slice(&tail[pos + 4..body_end]);
}
return;
}
let advance = usize::from(nextptr) * 4;
if advance < 4 {
return;
}
let next_pos = pos.saturating_add(advance).min(tail.len());
if pos + 4 < next_pos {
target.extend_from_slice(&tail[pos + 4..next_pos]);
}
if next_pos >= tail.len() {
return;
}
pos = next_pos;
}
}
fn emit_chunk_chain(
target: &mut Vec<u8>,
target_z_base: &mut Vec<i32>,
slab_buf: &[u8],
off: usize,
world_z_base: i32,
strip_trailing_bedrock: bool,
mip_level: u32,
) -> Option<usize> {
if off >= slab_buf.len() {
return None;
}
let tail = &slab_buf[off..];
let start_target_len = target.len();
let mut pos = 0usize;
let mut prev_slab_target_pos: Option<usize> = None;
let mut last_slab_target_pos: Option<usize> = None;
loop {
if pos + 4 > tail.len() {
break;
}
let nextptr = tail[pos];
let slab_target_pos = target.len();
if nextptr == 0 {
let z1 = tail[pos + 1];
let bedrock_sentinel = 0xff_u8.wrapping_shr(mip_level);
if strip_trailing_bedrock && z1 == bedrock_sentinel {
if let Some(prev) = prev_slab_target_pos {
target[prev] = 0;
last_slab_target_pos = Some(prev);
} else {
target.truncate(start_target_len);
target_z_base.truncate(start_target_len);
return None;
}
} else {
let z1_i = i32::from(z1);
let z1c_i = i32::from(tail[pos + 2]);
let n_floor = usize::try_from((z1c_i - z1_i + 1).max(0)).unwrap_or(0);
let last_size = 4 + n_floor * 4;
let end = (pos + last_size).min(tail.len());
target.extend_from_slice(&tail[pos..end]);
last_slab_target_pos = Some(slab_target_pos);
}
break;
}
let advance = usize::from(nextptr) * 4;
if advance < 4 {
if target.len() > start_target_len {
} else {
return None;
}
break;
}
let next_pos = pos.saturating_add(advance).min(tail.len());
target.extend_from_slice(&tail[pos..next_pos]);
prev_slab_target_pos = Some(slab_target_pos);
last_slab_target_pos = Some(slab_target_pos);
if next_pos >= tail.len() {
break;
}
pos = next_pos;
}
target_z_base.resize(target.len(), world_z_base);
last_slab_target_pos
}
fn build_owned_column_multi_chz<'a>(
target: &mut ColumnSource<'a>,
target_z_base: &mut Vec<i32>,
grid_view: crate::grid_view::GridView<'a>,
chunk_xy: [i32; 2],
chunk_local_xy: [i32; 2],
starting_chz: i32,
max_chz: i32,
chunk_size_z: i32,
mip_level: u32,
) {
target.clear();
target_z_base.clear();
if starting_chz == max_chz {
if let Some(chunk) = grid_view.chunk_at_xyz([chunk_xy[0], chunk_xy[1], starting_chz]) {
let chunk_size_at_mip = chunk.chunk_size_xy >> mip_level;
#[allow(clippy::cast_possible_wrap)]
let chunk_size_at_mip_mask = (chunk_size_at_mip as i32) - 1;
#[allow(clippy::cast_sign_loss)]
let lx = (chunk_local_xy[0] & chunk_size_at_mip_mask) as u32;
#[allow(clippy::cast_sign_loss)]
let ly = (chunk_local_xy[1] & chunk_size_at_mip_mask) as u32;
let mip_base = chunk.mip_base_offsets[mip_level as usize];
let col_idx = ly.wrapping_mul(chunk_size_at_mip).wrapping_add(lx);
let table_idx = mip_base + col_idx as usize;
if let Some(&col_off) = chunk.column_offsets.get(table_idx) {
let off = col_off as usize;
if off <= chunk.slab_buf.len() {
let chain_len = slab_chain_byte_len(&chunk.slab_buf[off..]);
target.set_borrowed(chunk.slab_buf, off, chain_len);
}
}
}
let _ = chunk_size_z;
return;
}
let target_vec = target.as_owned_mut();
let mut prev_last_slab_pos: Option<usize> = None;
for chz in starting_chz..=max_chz {
let Some(chunk) = grid_view.chunk_at_xyz([chunk_xy[0], chunk_xy[1], chz]) else {
continue;
};
let chunk_size_at_mip = chunk.chunk_size_xy >> mip_level;
#[allow(clippy::cast_possible_wrap)]
let chunk_size_at_mip_mask = (chunk_size_at_mip as i32) - 1;
#[allow(clippy::cast_sign_loss)]
let lx = (chunk_local_xy[0] & chunk_size_at_mip_mask) as u32;
#[allow(clippy::cast_sign_loss)]
let ly = (chunk_local_xy[1] & chunk_size_at_mip_mask) as u32;
let mip_base = chunk.mip_base_offsets[mip_level as usize];
let col_idx = ly.wrapping_mul(chunk_size_at_mip).wrapping_add(lx);
let table_idx = mip_base + col_idx as usize;
let Some(&col_off) = chunk.column_offsets.get(table_idx) else {
continue;
};
let world_z_base = chz * chunk_size_z;
let strip_trailing = chz < max_chz;
let chz_start = target_vec.len();
let Some(last_pos) = emit_chunk_chain(
target_vec,
target_z_base,
chunk.slab_buf,
col_off as usize,
world_z_base,
strip_trailing,
mip_level,
) else {
continue;
};
if let Some(prev_pos) = prev_last_slab_pos {
let advance_bytes = chz_start - prev_pos;
if advance_bytes % 4 == 0 && advance_bytes / 4 <= 255 {
#[allow(clippy::cast_possible_truncation)]
{
target_vec[prev_pos] = (advance_bytes / 4) as u8;
}
}
}
prev_last_slab_pos = Some(last_pos);
}
}
#[inline]
fn slab_z_at(state: &GrouscanState<'_>, vptr_offset: usize, byte: usize) -> i32 {
let idx = vptr_offset.saturating_add(byte);
let raw = i32::from(state.column.get(idx).copied().unwrap_or(0));
let base = if state.column_z_base.is_empty() {
state.chunk_world_z_base
} else {
state
.column_z_base
.get(idx)
.copied()
.unwrap_or(state.chunk_world_z_base)
};
raw + (base >> (state.gmipcnt as u32))
}
#[allow(clippy::cast_sign_loss, clippy::cast_possible_wrap)]
fn phase_remiporend(state: &mut GrouscanState<'_>) -> Phase {
if *TRACE_STARTSKY {
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];
if state.ixy_sptr_col_idx < mip_old_base {
return Phase::Startsky;
}
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 chunks_z = state.grid_view.chunk_grid.map_or(1u32, |cg| cg.chunks_z);
let advance = (((chunks_z * 512) >> old_mip) as usize) + 4;
let advance = advance.min(state.gylookup.len());
state.gylookup = &state.gylookup[advance..];
}
state.scratch.gixy[1] >>= 1;
state.cx_mip >>= 1;
state.cy_mip >>= 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]);
state.scratch.gpz[state.lane] =
state.scratch.gpz[state.lane].wrapping_add(state.scratch.gdz[state.lane]);
if !state.current_chunk_exists {
state.column.clear();
state.column_z_base.clear();
} else {
#[allow(clippy::cast_possible_wrap)]
let chunk_size_z_signed = state.chunk_size_z as i32;
let gmip = state.gmipcnt as u32;
let chunk_size_at_mip_mask = ((state.chunk_size_xy >> gmip) as i32) - 1;
let local_cx = state.cx_mip & chunk_size_at_mip_mask;
let local_cy = state.cy_mip & chunk_size_at_mip_mask;
build_owned_column_multi_chz(
&mut state.column,
&mut state.column_z_base,
state.grid_view,
state.current_chunk_idx_xy,
[local_cx, local_cy],
state.starting_chz,
state.max_chz,
chunk_size_z_signed,
gmip,
);
}
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 = *TRACE_STARTSKY;
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 {
state.scratch.skycast.col
};
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::*;
#[test]
fn vc2_builder_matches_bulk_slice_byte_for_byte() {
fn bulk_install(target: &mut Vec<u8>, slab_buf: &[u8], off: usize) {
target.clear();
if off >= slab_buf.len() {
return;
}
let tail = &slab_buf[off..];
let len = slab_chain_byte_len(tail);
target.extend_from_slice(&tail[..len]);
}
fn assert_match(slab_buf: &[u8], off: usize, label: &str) {
let mut bulk = Vec::new();
let mut built = Vec::new();
let mut built_z = Vec::new();
bulk_install(&mut bulk, slab_buf, off);
install_owned_column(&mut built, &mut built_z, slab_buf, off, 0);
assert_eq!(bulk, built, "{label}: bulk != builder output");
assert!(
built_z.is_empty(),
"{label}: z_base must stay empty for single-chz install (got len {})",
built_z.len()
);
}
let single = vec![0u8, 255, 255, 0, 0xAA, 0xBB, 0xCC, 0xDD];
assert_match(&single, 0, "single-slab bedrock");
let mut multi = vec![4u8, 0, 0, 0]; multi.extend_from_slice(&[
0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE, 0x11, 0x22, 0x33, 0x44,
]);
multi.extend_from_slice(&[0, 10, 12, 0]); multi.extend_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); assert_match(&multi, 0, "two-slab column");
let truncated_body = vec![0u8, 10, 12, 0, 1, 2, 3, 4];
assert_match(&truncated_body, 0, "last-slab body truncated");
let empty: Vec<u8> = vec![];
assert_match(&empty, 0, "empty slab_buf");
assert_match(&single, single.len() + 5, "off past end");
let malformed = vec![1u8, 99, 100, 101];
let mut bulk_malformed = Vec::new();
let mut built_malformed = Vec::new();
let mut built_z_malformed = Vec::new();
bulk_install(&mut bulk_malformed, &malformed, 0);
install_owned_column(
&mut built_malformed,
&mut built_z_malformed,
&malformed,
0,
0,
);
assert_eq!(bulk_malformed, malformed);
assert_eq!(built_malformed, malformed);
}
#[test]
fn vc3_translation_with_zero_base_is_identity() {
let single = vec![0u8, 100, 105, 0, 0xAA, 0xBB, 0xCC, 0xDD];
let mut multi = vec![4u8, 5, 10, 0]; multi.extend_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); multi.extend_from_slice(&[0, 50, 60, 40]); multi.extend_from_slice(&[
0x11, 0x12, 0x13, 0x14, 0x21, 0x22, 0x23, 0x24, 0x31, 0x32, 0x33, 0x34, 0x41, 0x42,
0x43, 0x44, 0x51, 0x52, 0x53, 0x54, 0x61, 0x62, 0x63, 0x64, 0x71, 0x72, 0x73, 0x74,
0x81, 0x82, 0x83, 0x84, 0x91, 0x92, 0x93, 0x94, 0xA1, 0xA2, 0xA3, 0xA4, 0xB1, 0xB2,
0xB3, 0xB4,
]);
for (label, slab_buf) in [("single", &single[..]), ("multi", &multi[..])] {
let mut untranslated = Vec::new();
let mut translated = Vec::new();
build_owned_column_from_chain(&mut untranslated, slab_buf, 0);
build_owned_column_from_chain_translated(&mut translated, slab_buf, 0, 0);
assert_eq!(
untranslated, translated,
"{label}: world_z_base=0 must produce byte-identical output"
);
}
}
#[test]
fn vc3_translation_with_positive_base_shifts_z_bytes() {
let mut col = vec![4u8, 5, 10, 0];
col.extend_from_slice(&[
0xA1, 0xA2, 0xA3, 0xA4, 0xB1, 0xB2, 0xB3, 0xB4, 0xC1, 0xC2, 0xC3, 0xC4,
]);
col.extend_from_slice(&[0, 20, 22, 18]);
col.extend_from_slice(&[
0xD1, 0xD2, 0xD3, 0xD4, 0xE1, 0xE2, 0xE3, 0xE4, 0xF1, 0xF2, 0xF3, 0xF4,
]);
let mut out = Vec::new();
build_owned_column_from_chain_translated(&mut out, &col, 0, 100);
assert_eq!(out[0], 4);
assert_eq!(out[1], 105);
assert_eq!(out[2], 110);
assert_eq!(out[3], 100);
assert_eq!(&out[4..16], &col[4..16]);
assert_eq!(out[16], 0);
assert_eq!(out[17], 120);
assert_eq!(out[18], 122);
assert_eq!(out[19], 118);
assert_eq!(&out[20..32], &col[20..32]);
assert_eq!(out.len(), col.len());
}
#[test]
#[should_panic(expected = "VC.3 z translation overflows u8")]
fn vc3_translation_overflow_panics() {
let col = vec![0u8, 200, 200, 0];
let mut out = Vec::new();
build_owned_column_from_chain_translated(&mut out, &col, 0, 100);
}
#[test]
fn vc4_install_leaves_column_z_base_empty_for_scalar_fallback() {
let mut col = vec![4u8, 0, 0, 0];
col.extend_from_slice(&[0xAA; 12]);
col.extend_from_slice(&[0, 10, 12, 0]);
col.extend_from_slice(&[0xBB; 12]);
for &base in &[0_i32, 256, -128, 1024] {
let mut column = Vec::new();
let mut z_base = Vec::new();
install_owned_column(&mut column, &mut z_base, &col, 0, base);
assert!(
z_base.is_empty(),
"z_base must stay empty for scalar fallback (base={base}, got len {})",
z_base.len()
);
let mut bulk = Vec::new();
build_owned_column_from_chain(&mut bulk, &col, 0);
assert_eq!(column, bulk, "column bytes drifted (base={base})");
}
}
#[test]
fn vc6_1_emit_chunk_chain_mip0_preserves_behaviour() {
let mut col = vec![4u8, 0, 0, 0];
col.extend_from_slice(&[0xAA; 12]); col.extend_from_slice(&[0, 0xff, 0xff, 0]);
let mut target = Vec::new();
let mut z_base = Vec::new();
let last_pos = emit_chunk_chain(&mut target, &mut z_base, &col, 0, 0, true, 0)
.expect("non-empty chain");
assert_eq!(target.len(), 16, "bedrock-stripped chain = first slab only");
assert_eq!(
target[0], 0,
"previous slab's nextptr rewritten to 0 (becomes new last)"
);
assert_eq!(
last_pos, 0,
"last_pos points at the new-last slab's nextptr"
);
assert_eq!(z_base.len(), target.len());
assert!(z_base.iter().all(|&v| v == 0));
}
#[test]
fn vc6_1_emit_chunk_chain_mip_n_strips_halved_bedrock_sentinel() {
let mut col = vec![4u8, 0, 0, 0];
col.extend_from_slice(&[0x11; 12]);
col.extend_from_slice(&[0, 63, 63, 0]);
{
let mut target = Vec::new();
let mut z_base = Vec::new();
let last_pos = emit_chunk_chain(&mut target, &mut z_base, &col, 0, 0, true, 2)
.expect("non-empty chain");
assert_eq!(target.len(), 16, "mip-2 bedrock stripped");
assert_eq!(target[0], 0);
assert_eq!(last_pos, 0);
}
{
let mut target = Vec::new();
let mut z_base = Vec::new();
emit_chunk_chain(&mut target, &mut z_base, &col, 0, 0, true, 0)
.expect("non-empty chain");
assert_eq!(
target.len(),
col.len(),
"mip-0 strip-check ignores the mip-N sentinel; chain emits in full"
);
}
}
#[test]
fn vc6_1_non_bedrock_last_slab_emit_is_mip_invariant() {
let mut col = vec![4u8, 0, 0, 0];
col.extend_from_slice(&[0x55; 12]);
col.extend_from_slice(&[0, 10, 12, 0]); col.extend_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
let mut prev: Option<Vec<u8>> = None;
for &mip in &[0u32, 1, 2, 3, 4] {
let mut target = Vec::new();
let mut z_base = Vec::new();
emit_chunk_chain(&mut target, &mut z_base, &col, 0, 0, true, mip)
.expect("non-empty chain");
if let Some(p) = prev.as_ref() {
assert_eq!(&target, p, "mip={mip} emit drifted vs mip=0/1/...");
}
prev = Some(target);
}
}
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,
chz_layer: 0,
};
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,
grid_view: crate::grid_view::GridView::from_parts(
64,
&DUMMY_SLAB_BUF,
&DUMMY_COLUMN_OFFSETS,
&DUMMY_MIP_OFFSETS,
),
camera_chunk_z: 0,
chunk_world_z_base: 0,
chunk_size_z: 256,
}
}
#[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, 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, 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, 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, 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, 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, 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,
grid_view: crate::grid_view::GridView::from_parts(
64,
&DUMMY_SLAB_BUF,
&DUMMY_COLUMN_OFFSETS,
&DUMMY_MIP_OFFSETS,
),
camera_chunk_z: 0,
chunk_world_z_base: 0,
chunk_size_z: 256,
};
GrouscanState::from_seed(scratch, &inputs, 0, 0, 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,
grid_view: crate::grid_view::GridView::from_parts(
64,
&DUMMY_SLAB_BUF,
&DUMMY_COLUMN_OFFSETS,
&DUMMY_MIP_OFFSETS,
),
camera_chunk_z: 0,
chunk_world_z_base: 0,
chunk_size_z: 256,
};
GrouscanState::from_seed(scratch, &inputs, vptr_offset, 0, 0, 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[0] = 8;
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[0] = 4;
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,
grid_view: crate::grid_view::GridView::from_parts(
64,
&DUMMY_SLAB_BUF,
&DUMMY_COLUMN_OFFSETS,
&DUMMY_MIP_OFFSETS,
),
camera_chunk_z: 0,
chunk_world_z_base: 0,
chunk_size_z: 256,
};
GrouscanState::from_seed(scratch, &inputs, vptr_offset, 0, 0, 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, 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::AfterDelete);
}
#[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, 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::AfterDelete);
}
#[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, 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, 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, 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, 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, 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, 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, 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, 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 mip_base = [0usize, column_offsets.len()];
let inputs = GrouscanInputs {
column: &slab_buf[20..], gylookup: &gylookup,
gcsub: &gcsub,
slab_buf: &slab_buf,
column_offsets: &column_offsets,
mip_base_offsets: &mip_base,
vsid: 4,
sky: None,
grid_view: crate::grid_view::GridView::from_parts(
4,
&slab_buf,
&column_offsets,
&mip_base,
),
camera_chunk_z: 0,
chunk_world_z_base: 0,
chunk_size_z: 256,
};
let mut s = fresh_scratch();
s.gixy = [1, 4]; let mut state = GrouscanState::from_seed(&mut s, &inputs, 0, 5, 1, 1, 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 mip_base = [0usize, column_offsets.len()];
let inputs = GrouscanInputs {
column: &slab_buf[..],
gylookup: &gylookup,
gcsub: &gcsub,
slab_buf: &slab_buf,
column_offsets: &column_offsets,
mip_base_offsets: &mip_base,
vsid: 4,
sky: None,
grid_view: crate::grid_view::GridView::from_parts(
4,
&slab_buf,
&column_offsets,
&mip_base,
),
camera_chunk_z: 0,
chunk_world_z_base: 0,
chunk_size_z: 256,
};
let mut s = fresh_scratch();
s.gpz = [0x100, 0x200];
let mut state = GrouscanState::from_seed(&mut s, &inputs, 0, 0, 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, 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, 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, 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, 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, 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 mip_base = [0usize, column_offsets.len()];
let inputs = GrouscanInputs {
column: &slab_buf[..],
gylookup: &gylookup,
gcsub: &gcsub,
slab_buf: &slab_buf,
column_offsets: &column_offsets,
mip_base_offsets: &mip_base,
vsid: 4,
sky: None,
grid_view: crate::grid_view::GridView::from_parts(
4,
&slab_buf,
&column_offsets,
&mip_base,
),
camera_chunk_z: 0,
chunk_world_z_base: 0,
chunk_size_z: 256,
};
let mut s = fresh_scratch();
let mut state = GrouscanState::from_seed(&mut s, &inputs, 0, 0, 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 mip_base = [0usize, column_offsets.len()];
let inputs = GrouscanInputs {
column: &slab_buf[..],
gylookup: &gylookup,
gcsub: &gcsub,
slab_buf: &slab_buf,
column_offsets: &column_offsets,
mip_base_offsets: &mip_base,
vsid: 4,
sky: None,
grid_view: crate::grid_view::GridView::from_parts(
4,
&slab_buf,
&column_offsets,
&mip_base,
),
camera_chunk_z: 0,
chunk_world_z_base: 0,
chunk_size_z: 256,
};
let mut s = fresh_scratch();
let mut state = GrouscanState::from_seed(&mut s, &inputs, 32, 0, 0, 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, 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,
chz_layer: 0,
};
let inputs = dummy_inputs();
let mut state = GrouscanState::from_seed(&mut s, &inputs, 0, 0, 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, 0, 0, 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, 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, 0, 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, 0, 0, 1, 1);
assert_eq!(p.lane, 0);
assert_eq!(p.ogx, 0x1234_0000_i32);
assert_eq!(p.gx, 0);
}
}