SDK example · full source

hello-pack

Reading and decompressing WORLD.PAK

On this page

What this program does

Find entries that cross sector boundaries, load raw and compressed chunks, verify their checksums and compare the decoded payloads against deterministic patterns. The program expects a specific fixture layout.

Build and run

Use the documented SDK checkout and tool setup. Run these commands from the repository root.

You can build the executable:

make example EXAMPLE=hello-pack

In this revision the source mentions make hello-pack-disc and tools/hello_pack_fixture.py, but neither is present in this SDK checkout. A generic make disc image will not pass this diagnostic.

The expected pack has 86 entries (IDs 0–85), with the boundary-straddling entry 84 holding a 3,000-byte raw pattern and entry 85 holding a 5,000-byte compressible pattern. The exact patterns are in the source below. A matching fixture must be supplied before running the test. Treat this page as a source reference. A missing-pack failure is not an emulator defect.

For original hardware, read the burned-disc requirements and warning.

Complete source

Complete, unabridged files from SDK revision f2fec1c301f1. Build inside the repository checkout: the manifest uses relative SDK paths, and some programs embed assets from elsewhere in the repository.

Runtime dependencies: psx-font, psx-gpu, psx-io, psx-pack, psx-rt, psx-vram.

Cargo.toml · 25 lines

View this file on GitHub

# SPDX-License-Identifier: GPL-2.0-or-later
[package]
name    = "hello-pack"
version = "0.1.0"
edition = "2021"
license = "GPL-2.0-or-later"

[workspace]

[[bin]]
name = "hello-pack"
path = "src/main.rs"

[profile.release]
panic = "abort"
codegen-units = 1
lto = true

[dependencies]
psx-gpu  = { path = "../../crates/psx-gpu" }
psx-rt   = { path = "../../crates/psx-rt" }
psx-font = { path = "../../crates/psx-font" }
psx-vram = { path = "../../crates/psx-vram" }
psx-pack = { path = "../../crates/psx-pack" }
psx-io   = { path = "../../crates/psx-io" }
src/main.rs · 271 lines

View this file on GitHub

//! `hello-pack` -- a `psx_pack::cd` smoke test that streams WORLD.PAK chunks
//! off the disc on boot and paints the result, so it can be verified
//! headlessly (dump the frame and read the PASS/FAIL banner, or grep the TTY
//! for `hello-pack: ALL PASS`).
//!
//! The disc is built by `make hello-pack-disc`: an 86-chunk fixture pack
//! (ids 0..=85) whose chunk table spans two sectors, so entry 84 straddles
//! the sector boundary and the reader's stitched entry parse runs against
//! real CD reads. Chunk 84 is an incompressible pattern (stored raw even
//! under `--world-pack-compress-rooms`); chunk 85 is a run pattern the
//! packer LZ4/HLZC-frames. Both patterns are recomputed here and must match
//! `tools/hello_pack_fixture.py` byte for byte.
//!
//! Steps, each shown as a line: prepare the reader, locate + load + FNV-check
//! + content-check the raw chunk, the same for the compressed chunk through
//! `load_chunk_decompressed`, and a lookup of an absent id. A green screen
//! with `ALL PASS` means table scan (straddle included), payload streaming,
//! checksums, and in-place decompression all work end-to-end off the disc.
//! A last line checks the reader put the interrupt mask back after its loads.

#![no_std]
#![no_main]

extern crate psx_rt;

use core::ptr::addr_of_mut;
use psx_font::{fonts::BASIC, FontAtlas};
use psx_gpu::display::{DisplayConfig, DoubleBuffer, Resolution, VideoMode};
use psx_gpu::Gpu;
use psx_pack::cd::{
    find_entry, load_chunk, load_chunk_decompressed, SectorReader, SECTOR_WORDS,
    WORLD_PACK_DEFAULT_LBA,
};
use psx_pack::fnv1a32;
use psx_rt::tty;
use psx_vram::{Clut, TextureDepth, TexturePage};

const FONT_TPAGE: TexturePage = TexturePage::new(320, 0, TextureDepth::Bit4);
const FONT_CLUT: Clut = Clut::new(320, 256);

const GREEN: (u8, u8, u8) = (80, 220, 100);
const RED: (u8, u8, u8) = (230, 80, 80);

// Fixture layout, in lockstep with tools/hello_pack_fixture.py.
const RAW_ID: u32 = 84; // its table entry straddles header sectors 0/1
const RAW_LEN: usize = 3000; // 2 sectors, partial tail
const COMP_ID: u32 = 85;
const COMP_LEN: usize = 5000; // raw length; stored HLZC-compressed

/// Incompressible pattern for chunk 84: murmur3-finalizer avalanche of the
/// index. (A plain multiplicative hash is not enough; its high bits form a
/// near-arithmetic sequence and LZ4 shrinks it, turning the chunk HLZC.)
fn raw_byte(k: usize) -> u8 {
    let mut h = (k as u32).wrapping_add(RAW_ID.wrapping_mul(2_654_435_761));
    h ^= h >> 16;
    h = h.wrapping_mul(0x85EB_CA6B);
    h ^= h >> 13;
    h = h.wrapping_mul(0xC2B2_AE35);
    h ^= h >> 16;
    h as u8
}

/// Compressible pattern for chunk 85 (64-byte runs).
fn comp_byte(k: usize) -> u8 {
    (((k >> 6) as u32 + COMP_ID) & 0xFF) as u8
}

// The reader owns a one-sector bounce buffer and dst must hold the biggest
// raw chunk plus the in-place LZ4 margin, so keep them off the 32 KiB stack.
static mut READER: Option<SectorReader> = None;
static mut SCRATCH: [u32; SECTOR_WORDS] = [0; SECTOR_WORDS];
const DST_WORDS: usize = 3072; // 12 KiB
static mut DST: [u32; DST_WORDS] = [0; DST_WORDS];

/// Up to this many result lines are recorded, then rendered every frame.
struct Report {
    lines: [Line; 12],
    len: usize,
    all_ok: bool,
    failed: &'static str,
}

#[derive(Copy, Clone)]
struct Line {
    text: &'static str,
    ok: bool,
}

impl Report {
    fn new() -> Self {
        Report {
            lines: [Line { text: "", ok: true }; 12],
            len: 0,
            all_ok: true,
            failed: "",
        }
    }

    /// Record one stage; echo it to TTY; `false` means stop the run.
    fn check(&mut self, label: &'static str, ok: bool) -> bool {
        if self.len < self.lines.len() {
            self.lines[self.len] = Line { text: label, ok };
            self.len += 1;
        }
        tty::print("hello-pack: ");
        tty::print(label);
        tty::println(if ok { " ok" } else { " FAIL" });
        if !ok {
            self.all_ok = false;
            if self.failed.is_empty() {
                self.failed = label;
            }
        }
        ok
    }
}

fn dst_bytes(dst: &[u32]) -> &[u8] {
    // SAFETY: [u32] viewed as its own bytes (little-endian target).
    unsafe { core::slice::from_raw_parts(dst.as_ptr() as *const u8, dst.len() * 4) }
}

fn bytes_match(bytes: &[u8], expect: fn(usize) -> u8) -> bool {
    let mut k = 0;
    while k < bytes.len() {
        if bytes[k] != expect(k) {
            return false;
        }
        k += 1;
    }
    true
}

/// Run the whole test and return the filled report.
fn run(rd: &mut SectorReader) -> Report {
    let mut rep = Report::new();
    // Set a mask bit no source raises here (SIO1), so the check at the end can
    // tell a restored mask from the VBlank-only one the reader runs under.
    psx_io::irq::set_mask(psx_io::irq::mask() | (1 << 8));
    let mask_before = psx_io::irq::mask();
    // SAFETY: main runs once; these statics are only touched here.
    let scratch = unsafe { &mut *addr_of_mut!(SCRATCH) };
    let dst = unsafe { &mut *addr_of_mut!(DST) };

    // The reader holds VBlank-only I_MASK from here to its last stop(), which
    // matches what psx_rt::interrupts installs for wait_vblank anyway.
    if !rep.check("PREPARE", rd.prepare()) {
        return rep;
    }

    // Raw chunk: its entry is the one that straddles header sectors 0/1.
    let Some(e84) = find_entry(rd, WORLD_PACK_DEFAULT_LBA, RAW_ID, scratch) else {
        rep.check("ENTRY 84", false);
        return rep;
    };
    if !rep.check(
        "ENTRY 84",
        e84.byte_size as usize == RAW_LEN && e84.sector_count == 2,
    ) {
        return rep;
    }
    let loaded = load_chunk(rd, WORLD_PACK_DEFAULT_LBA, RAW_ID, scratch, dst);
    if !rep.check("LOAD 84", loaded == Some(RAW_LEN)) {
        return rep;
    }
    if !rep.check(
        "FNV 84",
        fnv1a32(&dst_bytes(dst)[..RAW_LEN]) == e84.checksum,
    ) {
        return rep;
    }
    if !rep.check("DATA 84", bytes_match(&dst_bytes(dst)[..RAW_LEN], raw_byte)) {
        return rep;
    }

    // Compressed chunk: stored HLZC-framed, so the table byte_size must be
    // smaller than the raw length, and the stored bytes carry the checksum.
    let Some(e85) = find_entry(rd, WORLD_PACK_DEFAULT_LBA, COMP_ID, scratch) else {
        rep.check("ENTRY 85", false);
        return rep;
    };
    if !rep.check("ENTRY 85 HLZC", (e85.byte_size as usize) < COMP_LEN) {
        return rep;
    }
    let stored = load_chunk(rd, WORLD_PACK_DEFAULT_LBA, COMP_ID, scratch, dst);
    if !rep.check("LOAD 85", stored == Some(e85.byte_size as usize)) {
        return rep;
    }
    if !rep.check(
        "FNV 85",
        fnv1a32(&dst_bytes(dst)[..e85.byte_size as usize]) == e85.checksum,
    ) {
        return rep;
    }
    let raw = load_chunk_decompressed(rd, WORLD_PACK_DEFAULT_LBA, COMP_ID, scratch, dst);
    if !rep.check("LOAD-C 85", raw == Some(COMP_LEN)) {
        return rep;
    }
    if !rep.check(
        "DATA 85",
        bytes_match(&dst_bytes(dst)[..COMP_LEN], comp_byte),
    ) {
        return rep;
    }

    // A chunk id the pack does not contain must miss cleanly.
    let absent = find_entry(rd, WORLD_PACK_DEFAULT_LBA, 0xDEAD_BEEF, scratch).is_none();
    // Every load above ran under the reader's VBlank-only mask; stop() put the
    // one we set back. Read it before anything else runs: TTY output goes
    // through the BIOS, which the emulator's HLE leaves with I_MASK rewritten.
    let mask_after = psx_io::irq::mask();
    if !rep.check("ABSENT ID", absent) {
        return rep;
    }
    rep.check("IRQ MASK RESTORED", mask_after == mask_before);
    rep
}

#[no_mangle]
fn main() {
    let Some(peripherals) = psx_rt::Peripherals::take() else {
        return;
    };
    let mut gpu = Gpu::new(
        peripherals.gpu_dma,
        DisplayConfig::new(VideoMode::Ntsc, Resolution::R320X240),
    );
    let mut fb = DoubleBuffer::new(Resolution::R320X240);
    gpu.set_draw_area((0, 0), (319, 239));
    gpu.set_draw_offset((0, 0));
    let font = FontAtlas::upload(&BASIC, FONT_TPAGE, FONT_CLUT);

    // Run once at boot; hold the result on screen.
    // SAFETY: main runs once; the static is only touched here.
    let rd = unsafe { (*addr_of_mut!(READER)).insert(SectorReader::with_cd(peripherals.cd)) };
    let rep = run(rd);
    if rep.all_ok {
        tty::println("hello-pack: ALL PASS");
    } else {
        tty::print("hello-pack: FAIL ");
        tty::println(rep.failed);
    }

    loop {
        // Whole-screen verdict color so a headless --dump-hw is unambiguous.
        if rep.all_ok {
            fb.clear(&mut gpu, (16, 96, 32));
        } else {
            fb.clear(&mut gpu, (110, 20, 20));
        }
        font.draw_text(8, 6, "PSX-PACK CD STREAM TEST", (230, 230, 240));
        let mut y: i16 = 26;
        for i in 0..rep.len {
            let l = rep.lines[i];
            let tint = if l.ok { GREEN } else { RED };
            font.draw_text(8, y, if l.ok { "OK" } else { "XX" }, tint);
            font.draw_text(32, y, l.text, tint);
            y += 12;
        }
        if rep.all_ok {
            font.draw_text(8, 210, "ALL PASS", (235, 255, 235));
        } else {
            font.draw_text(8, 210, "FAIL", (255, 235, 235));
            font.draw_text(72, 210, rep.failed, (255, 235, 235));
        }

        gpu.wait_idle();
        psx_rt::interrupts::wait_vblank();
        fb.swap(&mut gpu);
    }
}