neurodoom 0.6.7

Deterministic no_std Doom engine with semantic and depth perception buffers for AI
Documentation
use neurodoom::map::MapData;
use neurodoom::math::*;
use neurodoom::render::sprites::ThingRef;
use neurodoom::render::{Renderer, SemanticClass};
use neurodoom::tables::FINESINE;
use neurodoom::texture::TextureData;
use neurodoom::wad::Wad;

fn load_wad() -> Vec<u8> {
    let path = std::env::var("DOOM_WAD")
        .unwrap_or_else(|_| "doom1.wad".to_string());
    std::fs::read(&path).unwrap_or_else(|e| panic!("failed to read WAD at {path}: {e}"))
}

fn setup() -> (MapData, TextureData, i32, i32, u32) {
    let data = load_wad();
    let wad = Wad::parse(&data).unwrap();
    let mut map = MapData::load(&wad, "E1M1").unwrap();
    let textures = TextureData::load(&wad).unwrap();
    map.resolve_textures(&textures);

    // Find player 1 start
    let p1 = map.things.iter().find(|t| t.type_num == 1).unwrap();
    let x = (p1.x as Fixed) << FRACBITS;
    let y = (p1.y as Fixed) << FRACBITS;
    let angle = ((p1.angle as u64) * ANG90 as u64 / 90) as u32; // convert degrees to BAM

    (map, textures, x, y, angle)
}

#[test]
fn render_e1m1_no_panic() {
    let (map, textures, x, y, angle) = setup();
    let mut renderer = Renderer::new();

    let view_z = map.sectors[map.subsectors[0].sector as usize].floor_height + 41 * FRACUNIT;
    renderer.render_player_view(&map, &textures, &neurodoom::render::ViewParams { x, y, z: view_z, angle });
}

#[test]
fn framebuffer_not_empty() {
    let (map, textures, x, y, angle) = setup();
    let mut renderer = Renderer::new();

    let view_z = map.sectors[map.subsectors[0].sector as usize].floor_height + 41 * FRACUNIT;
    renderer.render_player_view(&map, &textures, &neurodoom::render::ViewParams { x, y, z: view_z, angle });
    renderer.indexed_to_rgba(&textures);

    // RGBA buffer should have non-zero pixels
    let nonzero = renderer.rgba.iter().filter(|&&b| b != 0).count();
    assert!(nonzero > 1000, "framebuffer seems mostly empty: {nonzero} nonzero bytes");
}

#[test]
fn semantic_has_walls() {
    let (map, textures, x, y, angle) = setup();
    let mut renderer = Renderer::new();

    let view_z = map.sectors[map.subsectors[0].sector as usize].floor_height + 41 * FRACUNIT;
    renderer.render_player_view(&map, &textures, &neurodoom::render::ViewParams { x, y, z: view_z, angle });

    // Semantic buffer should contain Wall pixels
    let wall_count = renderer
        .semantic
        .iter()
        .filter(|&&c| c == SemanticClass::Wall as u8)
        .count();
    assert!(wall_count > 0, "no Wall pixels in semantic buffer");
}

#[test]
fn semantic_has_doors() {
    let (map, textures, x, y, angle) = setup();
    let mut renderer = Renderer::new();

    let view_z = map.sectors[map.subsectors[0].sector as usize].floor_height + 41 * FRACUNIT;
    renderer.render_player_view(&map, &textures, &neurodoom::render::ViewParams { x, y, z: view_z, angle });

    // E1M1 has doors with special linedefs — Door pixels should appear
    let door_count = renderer
        .semantic
        .iter()
        .filter(|&&c| c == SemanticClass::Door as u8)
        .count();
    // Door may not be visible from spawn, so just print for debugging
    println!("Door pixels: {door_count}");
}

#[test]
fn depth_buffer_has_values() {
    let (map, textures, x, y, angle) = setup();
    let mut renderer = Renderer::new();

    let view_z = map.sectors[map.subsectors[0].sector as usize].floor_height + 41 * FRACUNIT;
    renderer.render_player_view(&map, &textures, &neurodoom::render::ViewParams { x, y, z: view_z, angle });

    // Depth buffer should have non-zero values where walls are drawn
    let nonzero = renderer.depth.iter().filter(|&&d| d != 0).count();
    assert!(nonzero > 0, "depth buffer is all zeros");
}

#[test]
fn semantic_has_floor() {
    let (map, textures, x, y, angle) = setup();
    let mut renderer = Renderer::new();

    let view_z = map.sectors[map.subsectors[0].sector as usize].floor_height + 41 * FRACUNIT;
    renderer.render_player_view(&map, &textures, &neurodoom::render::ViewParams { x, y, z: view_z, angle });

    let floor_count = renderer
        .semantic
        .iter()
        .filter(|&&c| c == SemanticClass::Floor as u8)
        .count();
    assert!(floor_count > 0, "no Floor pixels in semantic buffer");
}

#[test]
fn semantic_has_ceiling() {
    let (map, textures, x, y, angle) = setup();
    let mut renderer = Renderer::new();

    let view_z = map.sectors[map.subsectors[0].sector as usize].floor_height + 41 * FRACUNIT;
    renderer.render_player_view(&map, &textures, &neurodoom::render::ViewParams { x, y, z: view_z, angle });

    let ceiling_count = renderer
        .semantic
        .iter()
        .filter(|&&c| c == SemanticClass::Ceiling as u8)
        .count();
    assert!(ceiling_count > 0, "no Ceiling pixels in semantic buffer");
}

#[test]
fn depth_increases_with_distance() {
    let (map, textures, x, y, angle) = setup();
    let mut renderer = Renderer::new();

    let view_z = map.sectors[map.subsectors[0].sector as usize].floor_height + 41 * FRACUNIT;
    renderer.render_player_view(&map, &textures, &neurodoom::render::ViewParams { x, y, z: view_z, angle });

    // Check that depth values at screen center are positive
    let center_x = SCREENWIDTH / 2;
    let center_y = SCREENHEIGHT / 2;
    let idx = center_y * SCREENWIDTH + center_x;
    let center_depth = renderer.depth[idx];
    println!("Center pixel depth: {center_depth}");
    // Depth should be positive if a wall is visible
    if renderer.semantic[idx] == SemanticClass::Wall as u8 {
        assert!(center_depth > 0, "wall at center should have positive depth");
    }
}

#[test]
fn sprites_render_items() {
    let (map, textures, x, y, angle) = setup();
    let mut renderer = Renderer::new();

    let view_z = map.sectors[map.subsectors[0].sector as usize].floor_height + 41 * FRACUNIT;
    renderer.render_player_view(&map, &textures, &neurodoom::render::ViewParams { x, y, z: view_z, angle });

    // Place thing 48 units ahead of the player
    let ang_idx = (angle >> ANGLETOFINESHIFT) as usize;
    let thing_x = x + fixed_mul(48 * FRACUNIT, neurodoom::math::finecosine(ang_idx));
    let thing_y = y + fixed_mul(48 * FRACUNIT, FINESINE[ang_idx]);
    let thing_z = map.sectors[map.subsectors[0].sector as usize].floor_height;

    // Reset occlusion so sprite can draw (test in isolation)
    renderer.floor_clip.fill(SCREENHEIGHT as i16);
    renderer.ceiling_clip.fill(-1);
    renderer.drawsegs.clear();

    assert!(!textures.sprites.is_empty(), "no sprite defs loaded");

    let things = [ThingRef {
        x: thing_x,
        y: thing_y,
        z: thing_z,
        angle: 0,
        sprite_num: 0,
        frame: 0,
        flags: 0,
        mobj_type: 100, // → SemanticClass::Item
        light_level: 160,
    }];

    renderer.draw_things(&things, &textures);

    assert!(!renderer.vissprites.is_empty(), "sprite was not projected");

    let item_count = renderer
        .semantic
        .iter()
        .filter(|&&c| c == SemanticClass::Item as u8)
        .count();
    assert!(item_count > 0, "no Item pixels drawn (vissprites: {})", renderer.vissprites.len());
}