neurodoom 0.6.7

Deterministic no_std Doom engine with semantic and depth perception buffers for AI
Documentation
// Per-row floor/ceiling span rasterizer on the hot path — see rationale
// in src/render/mod.rs.
#![allow(clippy::indexing_slicing)]

use crate::math::*;
use crate::tables::FINESINE;
use crate::texture::TextureData;

use super::*;

/// Distance-based light level constants.
pub const LIGHTZSHIFT: i32 = 20;
pub const MAXLIGHTZ: usize = 128;

/// Sky texture angle shift.
pub const ANGLETOSKYSHIFT: u32 = 22;

impl Renderer {
    /// Lookup tables for plane rendering.
    pub fn init_planes(&mut self) {
        // yslope: maps screen Y → distance multiplier
        let half_h = self.view_height / 2;
        for i in 0..SCREENHEIGHT {
            let dy = ((i as i32 - half_h) << FRACBITS) + FRACUNIT / 2;
            let dy = dy.abs().max(1);
            self.yslope[i] = fixed_div((self.view_width / 2) * FRACUNIT, dy);
        }

        // distscale: corrects for horizontal screen perspective
        for i in 0..SCREENWIDTH {
            let cos_adj = finecosine(
                (self.xtoviewangle[i] >> ANGLETOFINESHIFT) as usize & FINEMASK,
            )
            .abs()
            .max(1);
            self.distscale[i] = fixed_div(FRACUNIT, cos_adj);
        }
    }

    /// Initialize zlight tables (distance-based lighting for planes).
    pub fn init_zlight(&mut self) {
        const DISTMAP: i32 = 2;
        for i in 0..LIGHTLEVELS {
            let startmap =
                ((LIGHTLEVELS - 1 - i) * 2 * NUMCOLORMAPS / LIGHTLEVELS) as i32;
            for j in 0..MAXLIGHTZ {
                let scale = fixed_div(
                    (SCREENWIDTH as i32 / 2) * FRACUNIT,
                    (j as i32 + 1) << LIGHTZSHIFT,
                );
                let scale = scale >> LIGHTSCALESHIFT;
                let level = (startmap - scale / DISTMAP).clamp(0, NUMCOLORMAPS as i32 - 1);
                self.zlight[i][j] = level as usize;
            }
        }
    }

    /// Clear plane state at start of frame. Also sets basexscale/baseyscale.
    pub fn clear_planes(&mut self) {
        let angle = self
            .view_angle
            .wrapping_sub(ANG90)
            >> ANGLETOFINESHIFT;
        let angle = angle as usize & FINEMASK;
        self.basexscale = fixed_div(finecosine(angle), self.center_x_frac);
        self.baseyscale = -fixed_div(
            FINESINE[angle],
            self.center_x_frac,
        );
    }

    /// Find or create a visplane for (height, picnum, lightlevel).
    pub fn find_plane(&mut self, height: Fixed, picnum: i16, lightlevel: i16) -> usize {
        // Sky flats all merge
        let (h, ll) = if picnum == self.sky_flat_num {
            (0, 0)
        } else {
            (height, lightlevel)
        };

        // Search existing
        for i in 0..self.visplanes.len() {
            let pl = &self.visplanes[i];
            if pl.height == h && pl.pic_num == picnum && pl.light_level == ll {
                return i;
            }
        }

        // Create new
        self.visplanes.push(VisPlane::new(h, picnum, ll));
        self.visplanes.len() - 1
    }

    /// Extend a visplane's X range, or split if there's overlap.
    pub fn check_plane(&mut self, pl_idx: usize, start: i32, stop: i32) -> usize {
        let pl = &self.visplanes[pl_idx];
        let old_min = pl.min_x;
        let old_max = pl.max_x;

        // Compute intersection and union
        let intrl = start.max(old_min);
        let intrh = stop.min(old_max);
        let unionl = start.min(old_min);
        let unionh = stop.max(old_max);

        // Check for overlap in intersection
        let mut has_overlap = false;
        if intrl <= intrh {
            for x in intrl..=intrh {
                if self.visplanes[pl_idx].top[x as usize] != 0xFF {
                    has_overlap = true;
                    break;
                }
            }
        }

        if !has_overlap {
            // No overlap — extend
            self.visplanes[pl_idx].min_x = unionl;
            self.visplanes[pl_idx].max_x = unionh;
            return pl_idx;
        }

        // Overlap — create new plane with same properties
        let new_pl = VisPlane::new(
            self.visplanes[pl_idx].height,
            self.visplanes[pl_idx].pic_num,
            self.visplanes[pl_idx].light_level,
        );
        self.visplanes.push(new_pl);
        let new_idx = self.visplanes.len() - 1;
        self.visplanes[new_idx].min_x = start;
        self.visplanes[new_idx].max_x = stop;
        new_idx
    }

    /// Render all collected visplanes.
    pub fn draw_planes(&mut self, textures: &TextureData) {
        for pl_idx in 0..self.visplanes.len() {
            let pl = &self.visplanes[pl_idx];
            if pl.min_x > pl.max_x {
                continue;
            }

            let picnum = pl.pic_num;
            let height = pl.height;
            let light_level = pl.light_level;
            let minx = pl.min_x;
            let maxx = pl.max_x;

            if picnum == self.sky_flat_num {
                self.draw_sky_plane(textures, pl_idx);
                continue;
            }

            // Get flat texture data
            let flat_idx = picnum as usize;
            if flat_idx >= textures.flats.len() {
                continue;
            }

            let plane_height = (height - self.view_z).abs();
            let light = ((light_level >> LIGHTSEGSHIFT as i16) as i32 + self.extra_light)
                .clamp(0, LIGHTLEVELS as i32 - 1) as usize;

            let semantic_class = if height > self.view_z {
                SemanticClass::Ceiling
            } else {
                SemanticClass::Floor
            };

            // Span rendering: convert top[]/bottom[] arrays into horizontal spans
            // using R_MakeSpans algorithm. Uses a copy of the plane arrays with
            // sentinels at [minx-1] and [maxx+1] to avoid bounds issues.
            let mut spanstart = [0i32; SCREENHEIGHT];

            // Copy to a +2 array so we can safely index [minx-1] and [maxx+1]
            // Offset by 1: array index 0 = plane column -1
            let mut top = [0xFFu8; SCREENWIDTH + 2];
            let mut bottom = [0u8; SCREENWIDTH + 2];
            let off = 1usize; // offset: top[x + off] = plane's top[x]
            for x in minx..=maxx {
                top[x as usize + off] = self.visplanes[pl_idx].top[x as usize];
                bottom[x as usize + off] = self.visplanes[pl_idx].bottom[x as usize];
            }
            // Sentinels: top[minx-1] = 0xFF, top[maxx+1] = 0xFF
            if minx as usize + off > 0 {
                top[minx as usize + off - 1] = 0xFF;
            }
            if (maxx + 1) as usize + off < top.len() {
                top[(maxx + 1) as usize + off] = 0xFF;
            }

            // R_MakeSpans: iterate from minx to maxx+1 (inclusive)
            let stop = maxx + 1;
            for x in minx..=stop {
                let xu = x as usize + off;
                let t2 = top[xu] as i32;
                let b2 = bottom[xu] as i32;
                let t1 = top[xu - 1] as i32; // x-1, safe due to offset+sentinel
                let b1 = bottom[xu - 1] as i32;

                // Close spans that shrink at the top
                let mut ct = t1;
                while ct < t2 && ct <= b1 {
                    self.map_and_draw_span(
                        ct, spanstart[ct as usize], x - 1,
                        plane_height, light, flat_idx, textures, semantic_class,
                    );
                    ct += 1;
                }
                // Close spans that shrink at the bottom
                let mut cb = b1;
                while cb > b2 && cb >= t1 {
                    self.map_and_draw_span(
                        cb, spanstart[cb as usize], x - 1,
                        plane_height, light, flat_idx, textures, semantic_class,
                    );
                    cb -= 1;
                }
                // Open new spans that grow at the top
                let mut ot = t2;
                while ot < t1 && ot <= b2 {
                    spanstart[ot as usize] = x;
                    ot += 1;
                }
                // Open new spans that grow at the bottom
                let mut ob = b2;
                while ob > b1 && ob >= t2 {
                    spanstart[ob as usize] = x;
                    ob -= 1;
                }
            }
        }
    }

    /// Map a plane scanline and draw the span.
    #[allow(clippy::too_many_arguments)]
    fn map_and_draw_span(
        &mut self,
        y: i32,
        x1: i32,
        x2: i32,
        plane_height: Fixed,
        light: usize,
        flat_idx: usize,
        textures: &TextureData,
        semantic_class: SemanticClass,
    ) {
        if y < 0 || y >= self.view_height || x1 > x2 {
            return;
        }

        // Distance for this scanline
        let distance = fixed_mul(plane_height, self.yslope[y as usize]);

        // Texture stepping
        let ds_xstep = fixed_mul(distance, self.basexscale);
        let ds_ystep = fixed_mul(distance, self.baseyscale);

        // Texture start coordinates
        let length = fixed_mul(distance, self.distscale[x1.max(0) as usize]);
        let angle = self
            .view_angle
            .wrapping_add(self.xtoviewangle[x1.max(0) as usize])
            >> ANGLETOFINESHIFT;
        let angle = angle as usize;
        let ds_xfrac = self.view_x + fixed_mul(finecosine(angle & FINEMASK), length);
        let ds_yfrac = 0i32.wrapping_sub(self.view_y)
            - fixed_mul(FINESINE[angle & FINEMASK], length);

        // Light level based on distance
        let z_idx = ((distance >> LIGHTZSHIFT) as usize).min(MAXLIGHTZ - 1);
        let colormap_idx = self.zlight[light][z_idx];

        // Draw the span
        let flat = &textures.flats[flat_idx].pixels;
        let cmap = if colormap_idx < textures.colormaps.len() {
            &textures.colormaps[colormap_idx]
        } else if !textures.colormaps.is_empty() {
            &textures.colormaps[0]
        } else {
            return;
        };

        let mut xfrac = ds_xfrac;
        let mut yfrac = ds_yfrac;

        for x in x1.max(0)..=x2.min(SCREENWIDTH as i32 - 1) {
            let idx = (y * SCREENWIDTH as i32 + x) as usize;

            // 64x64 flat texture lookup
            let spot = (((yfrac >> 10) & (63 * 64)) + ((xfrac >> FRACBITS) & 63)) as usize;
            let spot = spot & 4095; // clamp to 64x64
            let pixel = flat[spot];

            self.screen[idx] = cmap[pixel as usize];
            self.semantic[idx] = semantic_class as u8;
            self.depth[idx] = distance;
            // Floors/ceilings do NOT block sprites — only walls do

            xfrac += ds_xstep;
            yfrac += ds_ystep;
        }
    }

    /// Draw a sky plane using the sky wall texture (column-based, full bright).
    fn draw_sky_plane(&mut self, textures: &TextureData, pl_idx: usize) {
        let pl = &self.visplanes[pl_idx];
        let minx = pl.min_x;
        let maxx = pl.max_x;

        // Find SKY1 wall texture
        let sky_tex_idx = self.sky_texture_num as usize;
        if sky_tex_idx >= textures.textures.len() {
            return;
        }
        let sky_tex = &textures.textures[sky_tex_idx];
        let sky_height = sky_tex.height as i32;
        if sky_height == 0 {
            return;
        }

        // Copy plane bounds
        let mut top = [0xFFu8; SCREENWIDTH];
        let mut bottom = [0u8; SCREENWIDTH];
        let lo = minx.max(0) as usize;
        let hi = (maxx as usize).min(SCREENWIDTH - 1) + 1;
        top[lo..hi].copy_from_slice(&self.visplanes[pl_idx].top[lo..hi]);
        bottom[lo..hi].copy_from_slice(&self.visplanes[pl_idx].bottom[lo..hi]);

        // Sky is always drawn full bright (colormap 0)
        let cmap = if !textures.colormaps.is_empty() {
            &textures.colormaps[0]
        } else {
            return;
        };

        // Sky texture mid: vertically centered
        let sky_texturemid = 100 * FRACUNIT;
        let sky_iscale = FRACUNIT; // 1:1 vertical scale

        for x in minx.max(0)..=maxx.min(SCREENWIDTH as i32 - 1) {
            let yl = top[x as usize] as i32;
            let yh = bottom[x as usize] as i32;
            if yl > yh || top[x as usize] == 0xFF {
                continue;
            }

            // Angle-based column selection (wraps sky texture around view)
            let angle = self
                .view_angle
                .wrapping_add(self.xtoviewangle[x as usize])
                >> ANGLETOSKYSHIFT;
            let col_idx = (angle as usize) % sky_tex.width as usize;
            let col_data = sky_tex.column(col_idx);

            // Draw column with vertical texture mapping
            let mut frac = sky_texturemid as i64 + (yl as i64 - self.center_y as i64) * sky_iscale as i64;

            for y in yl..=yh {
                let mut tex_y = ((frac >> FRACBITS) as i32) % sky_height;
                if tex_y < 0 { tex_y += sky_height; }
                let pixel = if (tex_y as usize) < col_data.len() {
                    col_data[tex_y as usize]
                } else {
                    0
                };
                let idx = (y * SCREENWIDTH as i32 + x) as usize;
                self.screen[idx] = cmap[pixel as usize];
                self.semantic[idx] = SemanticClass::Ceiling as u8;
                self.depth[idx] = 0;
                // Sky does not block sprites
                frac += sky_iscale as i64;
            }
        }
    }
}