use rayon::prelude::*;
use crate::camera_math;
use crate::camera_math::CameraState;
use crate::column_walk;
use crate::opticast_prelude;
use crate::opticast_prelude::OpticastPrelude;
use crate::projection;
use crate::rasterizer::{Rasterizer, ScratchPool};
use crate::ray_step;
use crate::scan_loops::{
bottom_quadrant, left_quadrant, right_quadrant, top_quadrant, ScanContext,
};
use crate::Camera;
#[derive(Debug, Clone, Copy)]
pub struct OpticastSettings {
pub xres: u32,
pub yres: u32,
pub y_start: u32,
pub y_end: u32,
pub hx: f32,
pub hy: f32,
pub hz: f32,
pub anginc: i32,
pub mip_levels: u32,
pub mip_scan_dist: i32,
pub max_scan_dist: i32,
}
impl OpticastSettings {
#[allow(clippy::cast_precision_loss)]
#[must_use]
pub fn for_oracle_framebuffer(width: u32, height: u32) -> Self {
let half_w = (width as f32) * 0.5;
let half_h = (height as f32) * 0.5;
Self {
xres: width,
yres: height,
y_start: 0,
y_end: height,
hx: half_w,
hy: half_h,
hz: half_w,
anginc: 1,
mip_levels: 1,
mip_scan_dist: 4,
max_scan_dist: 1024,
}
}
#[must_use]
pub fn with_y_range(mut self, y_start: u32, y_end: u32) -> Self {
self.y_start = y_start;
self.y_end = y_end;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OpticastOutcome {
Rendered,
SkippedCameraInSolid,
}
#[allow(clippy::too_many_arguments, clippy::cast_possible_wrap)]
#[must_use]
pub fn opticast<R: Rasterizer + Clone + Send + Sync>(
rasterizer: &mut R,
pool: &mut ScratchPool,
camera: &Camera,
settings: &OpticastSettings,
vsid: u32,
slab_buf: &[u8],
column_offsets: &[u32],
) -> OpticastOutcome {
let cs = camera_math::derive(
camera,
settings.xres,
settings.yres,
settings.hx,
settings.hy,
settings.hz,
);
let prelude = opticast_prelude::derive_prelude(
&cs,
vsid,
settings.mip_levels,
settings.mip_scan_dist,
settings.max_scan_dist,
);
let camera_column = camera_column_slice(slab_buf, column_offsets, prelude.column_index);
let Some(camera_column_data) = camera_column else {
return OpticastOutcome::SkippedCameraInSolid;
};
let Some((gstartz0, gstartz1, camera_vptr_offset)) =
column_walk::camera_column_air_gap(camera_column_data, prelude.li_pos[2])
else {
return OpticastOutcome::SkippedCameraInSolid;
};
let setup_proj = projection::derive_projection_with_y_range(
&cs,
settings.xres,
settings.yres,
settings.y_start,
settings.y_end,
settings.hx,
settings.hy,
settings.hz,
settings.anginc,
);
let setup_rs = ray_step::derive_ray_step(&cs, setup_proj.cx, setup_proj.cy, settings.hz);
let setup_ctx = ScanContext {
proj: &setup_proj,
rs: &setup_rs,
prelude: &prelude,
xres: settings.xres as i32,
y_start: settings.y_start as i32,
y_end: settings.y_end as i32,
anginc: settings.anginc,
camera_state: &cs,
camera_gstartz0: gstartz0,
camera_gstartz1: gstartz1,
camera_vptr_offset,
};
rasterizer.frame_setup(&setup_ctx);
let n_strips = pool.n_threads();
if n_strips <= 1 {
let scratch = pool.slot_mut(0);
top_quadrant(rasterizer, scratch, &setup_ctx);
right_quadrant(rasterizer, scratch, &setup_ctx);
bottom_quadrant(rasterizer, scratch, &setup_ctx);
left_quadrant(rasterizer, scratch, &setup_ctx);
} else {
run_strip_parallel(
rasterizer,
pool,
settings,
&cs,
&prelude,
gstartz0,
gstartz1,
camera_vptr_offset,
);
}
OpticastOutcome::Rendered
}
#[allow(clippy::too_many_arguments)]
fn run_strip_parallel<R: Rasterizer + Clone + Send + Sync>(
rasterizer: &mut R,
pool: &mut ScratchPool,
settings: &OpticastSettings,
cs: &CameraState,
prelude: &OpticastPrelude,
gstartz0: i32,
gstartz1: i32,
camera_vptr_offset: usize,
) {
let n_strips = pool.n_threads();
let y_start_total = settings.y_start;
let y_end_total = settings.y_end;
let span = y_end_total.saturating_sub(y_start_total);
if span == 0 {
return;
}
#[allow(clippy::cast_possible_truncation)]
let strip_height: u32 = ((span + n_strips as u32 - 1) / n_strips as u32).max(1);
let rasterizer_template = &*rasterizer;
let cs_ref: &CameraState = cs;
let prelude_ref: &OpticastPrelude = prelude;
let settings_ref: &OpticastSettings = settings;
let strip_body = |(i, scratch): (usize, &mut crate::rasterizer::ScanScratch)| {
#[allow(clippy::cast_possible_truncation)]
let strip_y_start = y_start_total.saturating_add((i as u32).saturating_mul(strip_height));
let strip_y_end = strip_y_start.saturating_add(strip_height).min(y_end_total);
if strip_y_start >= strip_y_end {
return;
}
let strip_proj = projection::derive_projection_with_y_range(
cs_ref,
settings_ref.xres,
settings_ref.yres,
strip_y_start,
strip_y_end,
settings_ref.hx,
settings_ref.hy,
settings_ref.hz,
settings_ref.anginc,
);
let strip_rs =
ray_step::derive_ray_step(cs_ref, strip_proj.cx, strip_proj.cy, settings_ref.hz);
#[allow(clippy::cast_possible_wrap)]
let strip_ctx = ScanContext {
proj: &strip_proj,
rs: &strip_rs,
prelude: prelude_ref,
xres: settings_ref.xres as i32,
y_start: strip_y_start as i32,
y_end: strip_y_end as i32,
anginc: settings_ref.anginc,
camera_state: cs_ref,
camera_gstartz0: gstartz0,
camera_gstartz1: gstartz1,
camera_vptr_offset,
};
let mut strip_rasterizer: R = rasterizer_template.clone();
top_quadrant(&mut strip_rasterizer, scratch, &strip_ctx);
right_quadrant(&mut strip_rasterizer, scratch, &strip_ctx);
bottom_quadrant(&mut strip_rasterizer, scratch, &strip_ctx);
left_quadrant(&mut strip_rasterizer, scratch, &strip_ctx);
};
pool.slots_mut_slice()
.par_iter_mut()
.enumerate()
.for_each(strip_body);
}
pub(crate) fn camera_column_slice<'a>(
slab_buf: &'a [u8],
column_offsets: &[u32],
idx: u32,
) -> Option<&'a [u8]> {
let i = idx as usize;
if i >= column_offsets.len() {
return None;
}
let start = column_offsets[i] as usize;
if start >= slab_buf.len() {
return None;
}
Some(&slab_buf[start..])
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rasterizer::ScanScratch;
#[derive(Debug, Default, Clone)]
struct Counts {
gline: u32,
hrend: u32,
vrend: u32,
}
impl Rasterizer for Counts {
fn gline(&mut self, _: &mut ScanScratch, _: u32, _: f32, _: f32, _: f32, _: f32) {
self.gline += 1;
}
fn hrend(&mut self, _: &mut ScanScratch, _: i32, _: i32, _: i32, _: i32, _: i32, _: i32) {
self.hrend += 1;
}
fn vrend(&mut self, _: &mut ScanScratch, _: i32, _: i32, _: i32, _: i32, _: i32) {
self.vrend += 1;
}
}
fn solid_slab_z200_to_254() -> Vec<u8> {
vec![0, 200, 254, 0]
}
fn looking_down_camera() -> Camera {
Camera {
pos: [1024.0, 1024.0, 128.0],
right: [1.0, 0.0, 0.0],
down: [0.0, 1.0, 0.0],
forward: [0.0, 0.0, 1.0],
}
}
#[allow(clippy::cast_possible_truncation)]
fn synthetic_world_with_camera_column(
column_data: &[u8],
camera_column_index: u32,
vsid: u32,
) -> (Vec<u8>, Vec<u32>) {
let vsid_sq = (vsid as usize) * (vsid as usize);
let len_u32 = column_data.len() as u32;
let cam_idx = camera_column_index as usize;
let mut column_offsets = vec![0u32; vsid_sq + 1];
for offset in &mut column_offsets[(cam_idx + 1)..] {
*offset = len_u32;
}
(column_data.to_vec(), column_offsets)
}
const LOOKING_DOWN_COL_INDEX: u32 = 1024 * 2048 + 1024;
#[test]
fn opticast_dispatches_all_four_quadrants() {
let cam = looking_down_camera();
let settings = OpticastSettings::for_oracle_framebuffer(640, 480);
let mut counts = Counts::default();
let mut pool = ScratchPool::new(640, 480, 2048);
let (slab_buf, column_offsets) = synthetic_world_with_camera_column(
&solid_slab_z200_to_254(),
LOOKING_DOWN_COL_INDEX,
2048,
);
let outcome = opticast(
&mut counts,
&mut pool,
&cam,
&settings,
2048,
&slab_buf,
&column_offsets,
);
assert_eq!(outcome, OpticastOutcome::Rendered);
assert!(counts.gline > 0, "expected ≥ 1 gline call");
assert!(counts.hrend > 0, "expected ≥ 1 hrend call");
assert!(counts.vrend > 0, "expected ≥ 1 vrend call");
}
#[test]
fn opticast_skips_when_camera_in_solid() {
let mut cam = looking_down_camera();
cam.pos[2] = 220.0;
let settings = OpticastSettings::for_oracle_framebuffer(640, 480);
let mut counts = Counts::default();
let mut pool = ScratchPool::new(640, 480, 2048);
let (slab_buf, column_offsets) = synthetic_world_with_camera_column(
&solid_slab_z200_to_254(),
LOOKING_DOWN_COL_INDEX,
2048,
);
let outcome = opticast(
&mut counts,
&mut pool,
&cam,
&settings,
2048,
&slab_buf,
&column_offsets,
);
assert_eq!(outcome, OpticastOutcome::SkippedCameraInSolid);
assert_eq!(counts.gline, 0);
assert_eq!(counts.hrend, 0);
assert_eq!(counts.vrend, 0);
}
#[test]
fn for_oracle_framebuffer_defaults() {
let s = OpticastSettings::for_oracle_framebuffer(640, 480);
assert_eq!(s.xres, 640);
assert_eq!(s.yres, 480);
assert!((s.hx - 320.0).abs() < f32::EPSILON);
assert!((s.hy - 240.0).abs() < f32::EPSILON);
assert!((s.hz - 320.0).abs() < f32::EPSILON);
assert_eq!(s.anginc, 1);
assert_eq!(s.mip_levels, 1);
assert_eq!(s.max_scan_dist, 1024);
}
}