SDK example · full source

hello-present-queue

Whole frames through the present queue

On this page

What this program does

Publish whole frames through psx-rt's VBlank-kicked present queue (the present-queue feature). Each frame is one chain: a recorded preamble (draw target and clear), the ordering table, a recorded HUD band, then GP0(1Fh). The CPU never kicks or flips: the VBlank handler shows the previous frame and kicks the next chain on the first edge that finds the one before it drawn. Everything a chain links is double buffered, and wait_arena_free keeps the CPU off the side a walk may still read.

Build and run

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

make disc EXAMPLE=hello-present-queue

Open build/examples/mipsel-sony-psx/release/hello-present-queue.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-io, psx-rt, psx-vram.

Cargo.toml · 24 lines

View this file on GitHub

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

[workspace]

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

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

[dependencies]
psx-gpu  = { path = "../../crates/psx-gpu" }
psx-io   = { path = "../../crates/psx-io" }
psx-rt   = { path = "../../crates/psx-rt", features = ["present-queue"] }
psx-font = { path = "../../crates/psx-font" }
psx-vram = { path = "../../crates/psx-vram" }
src/main.rs · 234 lines

View this file on GitHub

//! `hello-present-queue` -- whole frames through psx-rt's VBlank-kicked
//! present queue (`psx_rt::present`, the `present-queue` feature).
//!
//! `FRAMES` frames of varying cost are each published as one chain: a
//! recorded preamble (draw target and clear), the ordering table, a recorded
//! HUD band, then GP0(1Fh). The CPU never kicks or flips: the VBlank handler
//! shows the previous frame and kicks the next chain on the first edge that
//! finds the one before it drawn. Everything a chain links is double
//! buffered, and `wait_arena_free` keeps the CPU off the side a walk may
//! still read.
//!
//! Every frame must be kicked exactly once, with no stall recovery and no
//! recording overflow. The verdict goes to the TTY
//! (`PRESENT-QUEUE PASS ...` or `PRESENT-QUEUE FAIL ...`) and on screen.

#![no_std]
#![no_main]

extern crate psx_rt;

use core::ptr::addr_of_mut;
use psx_font::{fonts::BASIC, FontAtlas};
use psx_gpu::chain::DRAW_DONE_NODE;
use psx_gpu::display::{DisplayConfig, DoubleBuffer, Resolution, VideoMode};
use psx_gpu::ot::OrderingTable;
use psx_gpu::prim::{FillRect, TriGouraud};
use psx_gpu::Gpu;
use psx_io::gpu::start_recording_raw;
use psx_rt::{interrupts, present, 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);

/// Frames to publish.
const FRAMES: u32 = 120;
/// VBlanks the final screen's flip is given.
const WAIT_LIMIT: u32 = 8;
/// Most triangles in one frame; frame `f` draws `f % (TRIS + 1)`.
const TRIS: usize = 12;

const EMPTY: TriGouraud = TriGouraud::new([(0, 0); 3], [(0, 0, 0); 3]);
// Frame N is built while N-1 may still walk, so every buffer a chain links
// comes in two.
static mut TABLES: [OrderingTable<8>; 2] = [OrderingTable::new(), OrderingTable::new()];
static mut PACKETS: [[TriGouraud; TRIS]; 2] = [[EMPTY; TRIS], [EMPTY; TRIS]];
static mut PREAMBLES: [[u32; 64]; 2] = [[0; 64]; 2];
static mut HUDS: [[u32; 1024]; 2] = [[0; 1024]; 2];

/// Wait until the queued flip is applied or `limit` VBlanks pass.
fn wait_flip(limit: u32) -> bool {
    let start = interrupts::vblank_count();
    while interrupts::is_display_control_queued() {
        if interrupts::vblank_count().wrapping_sub(start) > limit {
            return false;
        }
    }
    true
}

/// Build frame `f`'s triangles: `f % (TRIS + 1)` half-screen Gouraud
/// triangles in slots 1..=6, leaving slot 0 for the HUD link.
fn build_frame(table: &mut OrderingTable<8>, packets: &mut [TriGouraud; TRIS], f: u32) {
    let mut frame = table.frame();
    let count = (f as usize) % (TRIS + 1);
    for (t, packet) in packets.iter_mut().take(count).enumerate() {
        let shade = ((t as u32 * 20 + f * 3) & 0x7F) as u8 + 0x20;
        let verts = if t & 1 == 0 {
            [(0, 0), (319, 0), (0, 239)]
        } else {
            [(319, 239), (0, 239), (319, 0)]
        };
        *packet = TriGouraud::new(verts, [(shade, 0, 40), (0, shade, 40), (40, 0, shade)]);
        frame.add(1 + t % 6, packet);
    }
}

/// `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() {
    interrupts::install_vblank_counter();
    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);
    fb.apply_draw_target(&mut gpu);
    let font = FontAtlas::upload(&BASIC, FONT_TPAGE, FONT_CLUT);
    gpu.wait_idle();

    let before = present::stats();
    present::start();
    let mut overflows = 0u32;
    // GP1(05h) word of the last published frame; 0 before the first.
    let mut shown = 0u32;
    let start = interrupts::vblank_count();
    for f in 0..FRAMES {
        let side = (f & 1) as usize;
        // The chain two frames back linked this side's buffers.
        present::wait_arena_free();
        // SAFETY: `wait_arena_free` guarantees no walk reads this side, and
        // these are the only references made to it this frame.
        let (table, packets, preamble, hud) = unsafe {
            (
                &mut (*addr_of_mut!(TABLES))[side],
                &mut (*addr_of_mut!(PACKETS))[side],
                &mut (*addr_of_mut!(PREAMBLES))[side],
                &mut (*addr_of_mut!(HUDS))[side],
            )
        };
        build_frame(table, packets, f);

        // SAFETY: `hud` is static, and is not written again until
        // `wait_arena_free` says its walk has ended.
        let recording = unsafe { start_recording_raw(hud.as_mut_ptr(), hud.len()) };
        gpu.draw(&FillRect::new(
            (0, fb.draw_origin().1 + 200),
            (320, 40),
            (20, 24, 60),
        ));
        font.draw_text(8, 212, "QUEUED HUD", WHITE);
        match recording.end() {
            // SAFETY: the recording and the static GP0(1Fh) node live as long
            // as `hud`; the table's slot 0 is still empty.
            Ok(Some(recording)) => unsafe {
                recording.link_to(DRAW_DONE_NODE.as_ptr());
                table.end_with_chain(recording.head());
            },
            _ => {
                overflows += 1;
                table.end_with_draw_done();
            }
        }

        // SAFETY: as for `hud`.
        let recording = unsafe { start_recording_raw(preamble.as_mut_ptr(), preamble.len()) };
        fb.apply_draw_target(&mut gpu);
        fb.clear(&mut gpu, (8, 24, 8));
        let Ok(Some(preamble)) = recording.end() else {
            overflows += 1;
            continue;
        };
        // SAFETY: the table is static and stays untouched until its walk ends.
        unsafe { preamble.link_to(table.submit_head()) };

        // Show the frame before this one when this one starts drawing.
        let display = shown;
        shown = fb.begin_deferred_swap();
        present::wait_slot_empty();
        // SAFETY: the slot is empty; the chain (preamble, table, packets,
        // HUD, GP0(1Fh)) is static and not rebuilt before `wait_arena_free`
        // clears its side two frames from now.
        unsafe { present::publish_raw(preamble.head(), display) };
    }
    present::wait_idle();
    psx_io::gpu::write_display_control(shown);
    let vblanks = interrupts::vblank_count().wrapping_sub(start);
    let after = present::stats();
    let kicks = after.kick_count.wrapping_sub(before.kick_count);
    let skips = after.skip_count.wrapping_sub(before.skip_count);
    let recoveries = after.recovery_count.wrapping_sub(before.recovery_count);

    let checks = [
        (kicks == FRAMES, "queued frames not each kicked once"),
        (recoveries == 0, "present queue stalled"),
        (overflows == 0, "recording overflowed"),
        (vblanks >= FRAMES, "fewer VBlanks than frames"),
        (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("PRESENT-QUEUE check failed: ");
        tty::println(what);
    }
    let lines = [
        line("failures", failures),
        line("frames", FRAMES),
        line("kicks", kicks),
        line("skips", skips),
        line("vblanks", vblanks),
    ];
    let (banner, tint) = if failures == 0 {
        ("PRESENT-QUEUE PASS", GREEN)
    } else {
        ("PRESENT-QUEUE FAIL", RED)
    };
    tty::print(banner);
    for (text, len) in &lines {
        tty::print(" ");
        tty::print(as_str(&text[..*len]));
    }
    tty::println("");

    loop {
        fb.apply_draw_target(&mut gpu);
        fb.clear(&mut gpu, (10, 12, 20));
        font.draw_text(8, 6, "WHOLE FRAMES KICKED AT VBLANK", 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 = 126;
        for (_, what) in checks.iter().filter(|check| !check.0) {
            font.draw_text(8, y, what, RED);
            y += 12;
        }
        gpu.arm_draw_done();
        gpu.signal_draw_done();
        interrupts::queue_display_control_at_vblank(fb.begin_deferred_swap());
        wait_flip(WAIT_LIMIT);
    }
}