SDK example · full source

hello-gteirq

Geometry commands interrupted by VBlank

On this page

What this program does

Exercise RTPS while VBlank interrupts arrive and check that the runtime exception path does not execute a geometry command twice. This is a runtime regression probe; a passing emulator run alone is not a new hardware conformance result.

Build and run

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

make disc EXAMPLE=hello-gteirq

Open build/examples/mipsel-sony-psx/release/hello-gteirq.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-gteirq"
version = "0.1.0"
edition = "2021"
license = "GPL-2.0-or-later"

[workspace]

[[bin]]
name = "hello-gteirq"
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 · 215 lines

View this file on GitHub

//! `hello-gteirq` -- does a GTE command run exactly once when a VBlank
//! interrupt lands on it?
//!
//! On silicon an interrupt taken on a GTE command lets the command run and
//! still reports EPC at it, so a handler that returns to EPC runs it twice
//! (hardware-tests v1.24, case 0xC9: 38 of 38). psx-rt's handler steps over
//! a GTE command at EPC instead (case 0xCB: 61 skips, none doubled or lost).
//! This example runs the v1.24 probe's pattern under psx-rt's own handler:
//! load SXY0-2 with three markers, RTPS, wait out its latency, read SXY0.
//! One RTPS shifts the screen-XY FIFO once, so SXY0 must hold the second
//! marker; two shifts leave the third, none the first. The markers' high
//! halves lie outside the range RTPS saturates to, so no projection can
//! pass for one. The loop runs for `VBLANKS` VBlank interrupts.
//!
//! The verdict goes to the TTY (`GTEIRQ PASS ...` or `GTEIRQ FAIL ...`) and
//! on screen. `gte_skips` counts the interrupts the handler stepped over a
//! GTE command for: about one in forty VBlanks on silicon, where the loop is
//! mostly RTPS-adjacent instructions. Since emulator commit c7e3ea8 PSoXide
//! also takes an interrupt with EPC on a GTE command, as silicon does, so
//! the probe can fail there too; a console run remains the reference.

#![no_std]
#![no_main]
#![feature(asm_experimental_arch)]

extern crate psx_rt;

use psx_font::{fonts::BASIC, FontAtlas};
use psx_gpu::display::{DisplayConfig, DoubleBuffer, Resolution, VideoMode};
use psx_gpu::Gpu;
use psx_rt::interrupts;
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 WHITE: (u8, u8, u8) = (220, 220, 230);
const GREEN: (u8, u8, u8) = (80, 220, 100);
const RED: (u8, u8, u8) = (230, 80, 80);

/// Written to SXY0/SXY1/SXY2 before each RTPS. After one RTPS SXY0 holds
/// B, after two C, after none A.
const SXY_A: u32 = 0x5A5A_1111;
const SXY_B: u32 = 0x5A5A_2222;
const SXY_C: u32 = 0x5A5A_3333;
/// Iterations between checks of the VBlank count.
const CHUNK: u32 = 4096;
/// Interrupts to run under: about five seconds on NTSC.
const VBLANKS: u32 = 300;

#[derive(Clone, Copy, Default)]
struct Counts {
    ok: u32,
    doubled: u32,
    missing: u32,
    other: u32,
}

/// `iterations` x (reload SXY0-2, RTPS, wait it out, classify SXY0).
///
/// The RTPS never sits in a delay slot (psx-rt's fix cannot cover one).
/// Twenty nops cover its latency before the read, so a stale read cannot
/// pass for a missing command.
fn gte_loop(iterations: u32, counts: &mut Counts) {
    let (mut ok, mut doubled, mut missing, mut other) =
        (counts.ok, counts.doubled, counts.missing, counts.other);
    unsafe {
        core::arch::asm!(
            ".set noreorder",
            "1:",
            ".word 0x48896000", // mtc2 $9, SXY0
            ".word 0x488A6800", // mtc2 $10, SXY1
            ".word 0x488B7000", // mtc2 $11, SXY2
            "nop",
            "nop",
            ".word 0x4A180001", // RTPS
            ".rept 20",
            "nop",
            ".endr",
            ".word 0x480C6000", // mfc2 $12, SXY0
            "nop",
            "beq $12, $10, 2f",
            "addiu $8, $8, -1",
            "beq $12, $11, 3f",
            "nop",
            "beq $12, $9, 4f",
            "nop",
            "addiu $24, $24, 1",
            "b 5f",
            "nop",
            "2:",
            "addiu $13, $13, 1",
            "b 5f",
            "nop",
            "3:",
            "addiu $14, $14, 1",
            "b 5f",
            "nop",
            "4:",
            "addiu $15, $15, 1",
            "5:",
            "bnez $8, 1b",
            "nop",
            ".set reorder",
            inout("$8") iterations => _,
            in("$9") SXY_A,
            in("$10") SXY_B,
            in("$11") SXY_C,
            lateout("$12") _,
            inout("$13") ok,
            inout("$14") doubled,
            inout("$15") missing,
            inout("$24") other,
            options(nostack)
        );
    }
    counts.ok = ok;
    counts.doubled = doubled;
    counts.missing = missing;
    counts.other = other;
}

/// `label=XXXXXXXX` for the TTY and the screen.
fn line(label: &str, value: u32) -> ([u8; 32], usize) {
    const DIGITS: &[u8; 16] = b"0123456789ABCDEF";
    let mut text = [b' '; 32];
    let label = &label.as_bytes()[..label.len().min(22)];
    text[..label.len()].copy_from_slice(label);
    text[label.len()] = b'=';
    for i in 0..8 {
        text[label.len() + 1 + i] = DIGITS[(value >> (28 - 4 * i) & 0xF) as usize];
    }
    (text, label.len() + 9)
}

fn as_str(text: &[u8]) -> &str {
    core::str::from_utf8(text).unwrap_or("?")
}

#[no_mangle]
fn main() {
    // Also sets SR.CU2, which the GTE needs.
    interrupts::install_vblank_counter();

    let skips_before = interrupts::gte_skip_count();
    let start = interrupts::vblank_count();
    let mut counts = Counts::default();
    let mut iterations = 0u32;
    while interrupts::vblank_count().wrapping_sub(start) < VBLANKS {
        gte_loop(CHUNK, &mut counts);
        iterations = iterations.wrapping_add(CHUNK);
    }
    let vblanks = interrupts::vblank_count().wrapping_sub(start);
    let skips = interrupts::gte_skip_count().wrapping_sub(skips_before);

    let checks = [
        (counts.doubled == 0, "an RTPS ran twice"),
        (counts.missing == 0, "an RTPS was lost"),
        (counts.other == 0, "SXY0 held no marker"),
        (counts.ok == iterations, "not every iteration counted"),
        (interrupts::fault_count() == 0, "exceptions other than IRQs"),
    ];
    let failures = checks.iter().filter(|check| !check.0).count() as u32;
    for (_, what) in checks.iter().filter(|check| !check.0) {
        tty::print("GTEIRQ check failed: ");
        tty::println(what);
    }
    let lines = [
        line("failures", failures),
        line("iterations", iterations),
        line("vblanks", vblanks),
        line("gte_skips", skips),
        line("doubled", counts.doubled),
        line("missing", counts.missing),
    ];
    let (banner, tint) = if failures == 0 {
        ("GTEIRQ PASS", GREEN)
    } else {
        ("GTEIRQ FAIL", RED)
    };
    tty::print(banner);
    for (text, len) in &lines {
        tty::print(" ");
        tty::print(as_str(&text[..*len]));
    }
    tty::println("");

    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);
    loop {
        fb.clear(&mut gpu, (10, 12, 20));
        font.draw_text(8, 6, "GTE COMMANDS UNDER VBLANK IRQS", WHITE);
        font.draw_text(8, 30, banner, tint);
        for (row, (text, len)) in lines.iter().enumerate() {
            font.draw_text(8, 54 + 12 * row as i16, as_str(&text[..*len]), WHITE);
        }
        let mut y = 138;
        for (_, what) in checks.iter().filter(|check| !check.0) {
            font.draw_text(8, y, what, RED);
            y += 12;
        }
        gpu.wait_idle();
        interrupts::wait_vblank();
        fb.swap(&mut gpu);
    }
}