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
# 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
//! `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);
}
}