SDK example · full source

hello-memprobe

Memory routine size and alignment checks

On this page

What this program does

Compare the runtime memory routines against reference loops across sizes and alignments. The program reports MEMPROBE PASS or FAIL on screen and through debug output. It's a diagnostic; don't use it as a game-loop template.

Build and run

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

make disc EXAMPLE=hello-memprobe

Open build/examples/mipsel-sony-psx/release/hello-memprobe.cue in the emulator with its sibling BIN in the same directory. The first-program walkthrough explains installation and loading.

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-rt, psx-vram.

Cargo.toml · 23 lines

View this file on GitHub

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

[workspace]

[[bin]]
name = "hello-memprobe"
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" }
src/main.rs · 302 lines

View this file on GitHub

//! `hello-memprobe` -- do `psx-rt`'s hand-scheduled `memcpy` / `memset` /
//! `memcmp` behave exactly like the C definitions on the target?
//!
//! Every size and alignment combination up to a few words past the block
//! loop, plus a few long copies, is run through the real symbols (sizes go
//! through `core::hint::black_box` so LLVM emits calls instead of inline
//! expansions) and checked bytewise against a reference loop, guard bytes
//! included. The verdict is printed on the TTY (`MEMPROBE PASS` /
//! `MEMPROBE FAIL <count>`) and drawn on screen.

#![no_std]
#![no_main]

extern crate psx_rt;

use core::hint::black_box;
use psx_font::{fonts::BASIC, FontAtlas};
use psx_gpu::display::{DisplayConfig, DoubleBuffer, Resolution, VideoMode};
use psx_gpu::Gpu;
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);

extern "C" {
    fn memcpy(dst: *mut u8, src: *const u8, n: usize) -> *mut u8;
    fn memset(dst: *mut u8, c: i32, n: usize) -> *mut u8;
    fn memcmp(a: *const u8, b: *const u8, n: usize) -> i32;
}

const BUF: usize = 1200;
const GUARD: u8 = 0xEE;

static mut SRC: [u8; BUF] = [0; BUF];
static mut DST: [u8; BUF] = [0; BUF];
/// First failures as (kind, a, b, n, bad): kind 1 memcpy, 2 memset, 3 memcmp.
static mut LOG: [[u32; 5]; 16] = [[0; 5]; 16];
static mut LOGGED: usize = 0;

fn log_failure(kind: u32, a: usize, b: usize, n: usize, bad: u32) {
    unsafe {
        if LOGGED < 16 {
            LOG[LOGGED] = [kind, a as u32, b as u32, n as u32, bad];
            LOGGED += 1;
        }
    }
}

fn fill_pattern(buf: &mut [u8], seed: u8) {
    let mut v = seed;
    for b in buf.iter_mut() {
        *b = v;
        v = v.wrapping_mul(31).wrapping_add(7);
    }
}

/// One memcpy of `n` bytes from `src + so` to `dst + do_`; returns the count
/// of wrong bytes (copied region and one guard word on each side).
fn check_memcpy(so: usize, do_: usize, n: usize) -> u32 {
    let (src, dst) = unsafe {
        (
            &mut *core::ptr::addr_of_mut!(SRC),
            &mut *core::ptr::addr_of_mut!(DST),
        )
    };
    fill_pattern(src, (so + n) as u8);
    for b in dst.iter_mut() {
        *b = GUARD;
    }
    let ret = unsafe {
        memcpy(
            dst.as_mut_ptr().add(do_),
            src.as_ptr().add(so),
            black_box(n),
        )
    };
    let mut bad = 0u32;
    if ret != unsafe { dst.as_mut_ptr().add(do_) } {
        bad += 1;
    }
    for i in 0..n {
        if dst[do_ + i] != src[so + i] {
            bad += 1;
        }
    }
    for i in do_.saturating_sub(4)..do_ {
        if dst[i] != GUARD {
            bad += 1;
        }
    }
    for i in (do_ + n)..(do_ + n + 4).min(BUF) {
        if dst[i] != GUARD {
            bad += 1;
        }
    }
    bad
}

fn check_memset(do_: usize, n: usize, value: u8) -> u32 {
    let dst = unsafe { &mut *core::ptr::addr_of_mut!(DST) };
    for b in dst.iter_mut() {
        *b = GUARD;
    }
    // The high bits of `c` must be ignored, as in C.
    let c = (value as i32) | 0x1234_5600;
    let ret = unsafe { memset(dst.as_mut_ptr().add(do_), black_box(c), black_box(n)) };
    let mut bad = 0u32;
    if ret != unsafe { dst.as_mut_ptr().add(do_) } {
        bad += 1;
    }
    for i in 0..n {
        if dst[do_ + i] != value {
            bad += 1;
        }
    }
    for i in do_.saturating_sub(4)..do_ {
        if dst[i] != GUARD {
            bad += 1;
        }
    }
    for i in (do_ + n)..(do_ + n + 4).min(BUF) {
        if dst[i] != GUARD {
            bad += 1;
        }
    }
    bad
}

fn reference_memcmp(a: &[u8], b: &[u8]) -> i32 {
    for i in 0..a.len() {
        if a[i] != b[i] {
            return a[i] as i32 - b[i] as i32;
        }
    }
    0
}

/// Compare `n` bytes at offsets `ao`/`bo`; `differ_at` (if < n) flips one
/// byte of `b` so the sign and position handling is exercised.
fn check_memcmp(ao: usize, bo: usize, n: usize, differ_at: usize, up: bool) -> u32 {
    let (a, b) = unsafe {
        (
            &mut *core::ptr::addr_of_mut!(SRC),
            &mut *core::ptr::addr_of_mut!(DST),
        )
    };
    fill_pattern(a, (n + differ_at) as u8);
    b.copy_from_slice(a);
    // Shift b so the two views hold the same bytes at different alignments.
    for i in 0..n {
        b[bo + i] = a[ao + i];
    }
    if differ_at < n {
        b[bo + differ_at] = if up {
            a[ao + differ_at].wrapping_add(1)
        } else {
            a[ao + differ_at].wrapping_sub(1)
        };
        // Keep the reference honest when the wrap changed the sign.
    }
    let got = unsafe { memcmp(a.as_ptr().add(ao), b.as_ptr().add(bo), black_box(n)) };
    let want = reference_memcmp(&a[ao..ao + n], &b[bo..bo + n]);
    let same_sign = (got == 0 && want == 0) || (got < 0 && want < 0) || (got > 0 && want > 0);
    if same_sign {
        0
    } else {
        let bytes = ((a[ao] as usize) << 24)
            | ((b[bo] as usize) << 16)
            | ((a[ao + n - 1] as usize) << 8)
            | (b[bo + n - 1] as usize);
        log_failure(
            4,
            got as u32 as usize,
            want as u32 as usize,
            bytes,
            (differ_at as u32) << 1 | u32::from(up),
        );
        1
    }
}

fn run_all() -> (u32, u32) {
    let mut cases = 0u32;
    let mut failures = 0u32;
    let sizes: [usize; 22] = [
        0, 1, 2, 3, 4, 5, 7, 8, 12, 15, 16, 17, 20, 31, 32, 33, 47, 48, 63, 64, 100, 257,
    ];
    for &n in sizes.iter() {
        for so in 0..4 {
            for do_ in 0..4 {
                cases += 1;
                let bad = check_memcpy(so + 8, do_ + 8, n);
                if bad != 0 {
                    failures += 1;
                    log_failure(1, so + 8, do_ + 8, n, bad);
                }
            }
        }
        for do_ in 0..4 {
            for value in [0x5Au8, 0x00] {
                cases += 1;
                let bad = check_memset(do_ + 8, n, value);
                if bad != 0 {
                    failures += 1;
                    log_failure(2, value as usize, do_ + 8, n, bad);
                }
            }
        }
        for ao in 0..4 {
            for bo in 0..4 {
                let positions = [n, 0, n / 2, n.saturating_sub(1), 4, 5, 9, 17];
                for &p in positions.iter() {
                    if p > n {
                        continue;
                    }
                    for up in [true, false] {
                        cases += 1;
                        let bad = check_memcmp(ao + 8, bo + 8, n, p, up);
                        if bad != 0 {
                            failures += 1;
                            log_failure(3, ao + 8, bo + 8, n, (p as u32) << 1 | u32::from(up));
                        }
                    }
                }
            }
        }
    }
    // Long copies past the block loop with every alignment pair.
    for &n in [1000usize, 1023, 1024, 1100].iter() {
        for so in 0..4 {
            for do_ in 0..4 {
                cases += 1;
                let bad = check_memcpy(so + 8, do_ + 8, n);
                if bad != 0 {
                    failures += 1;
                    log_failure(1, so + 8, do_ + 8, n, bad);
                }
            }
        }
        cases += 1;
        let bad = check_memset(9, n, 0xA5);
        if bad != 0 {
            failures += 1;
            log_failure(2, 0xA5, 9, n, bad);
        }
    }
    (cases, failures)
}

#[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);

    let (cases, failures) = run_all();
    if failures == 0 {
        tty::print("MEMPROBE PASS cases=");
        tty::print_hex_u32(cases);
        tty::println("");
    } else {
        tty::print("MEMPROBE FAIL ");
        tty::print_hex_u32(failures);
        tty::print(" of ");
        tty::print_hex_u32(cases);
        tty::println("");
        let logged = unsafe { LOGGED };
        for entry in unsafe { LOG[..logged].iter() } {
            tty::print("  kind/a/b/n/bad ");
            for value in entry.iter() {
                tty::print_hex_u32(*value);
                tty::print(" ");
            }
            tty::println("");
        }
    }

    loop {
        fb.clear(&mut gpu, (10, 12, 20));
        font.draw_text(8, 6, "MEM PROBE (memcpy/memset/memcmp)", (220, 220, 230));
        let (banner, tint) = if failures == 0 {
            ("ALL PASS", GREEN)
        } else {
            ("FAIL", RED)
        };
        font.draw_text(8, 30, banner, tint);
        gpu.wait_idle();
        psx_rt::interrupts::wait_vblank();
        fb.swap(&mut gpu);
    }
}