SDK example · full source

hello-present

Queued flips and GPU completion

On this page

What this program does

Queue display changes through the runtime and complete a frame with the GPU draw-done signal. Read the sequence around arm_draw_done, the closing command and the queued flip: a VBlank by itself does not prove the GPU finished drawing.

Build and run

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

make disc EXAMPLE=hello-present

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

[workspace]

[[bin]]
name = "hello-present"
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" }
psx-font = { path = "../../crates/psx-font" }
psx-vram = { path = "../../crates/psx-vram" }
src/main.rs · 243 lines

View this file on GitHub

//! `hello-present` -- psx-rt's queued display flip waits for the frame's
//! closing GP0(1Fh), not for GPUSTAT bit 28.
//!
//! On silicon bit 28 rises when the DMA has pushed a list's last packet,
//! about one large primitive before the drawing ends (hardware-tests v1.24
//! cases 219-226), so a flip gated on it can show a frame one primitive
//! short. psx-rt's VBlank handler now applies a queued display start only
//! once GPUSTAT bit 24 is set, which GP0(1Fh) raises when the GPU reaches
//! it. This example checks that contract end to end:
//!
//! 1. Held: a frame kicked without GP0(1Fh) keeps its flip queued, even
//!    after `wait_idle` has seen the GPU idle (the old bit-28 test would
//!    have flipped at the next edge).
//! 2. Released: `signal_draw_done` then lets the flip land within a few
//!    VBlanks.
//! 3. Pipelined: `FRAMES` frames of varying cost, each an ordering table
//!    closed with `end_with_draw_done`, armed with `arm_draw_done` and
//!    kicked asynchronously, flip through the queue with no timeout, and
//!    every flip is seen with its frame's GP0(1Fh) executed.
//!
//! The asynchronous kicks go through `FrameStorage::draw_async`: the table
//! and packets are `'static` and owned by the returned `InFlight` until
//! its `wait`, so no frame is rebuilt under a walk.
//!
//! The verdict goes to the TTY (`PRESENT PASS ...` or `PRESENT 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::display::{DisplayConfig, DoubleBuffer, Resolution, VideoMode};
use psx_gpu::frame::{FrameStorage, OtFrame};
use psx_gpu::prim::TriGouraud;
use psx_gpu::{is_draw_done, Gpu};
use psx_rt::interrupts;
use psx_rt::tty;
use psx_rt::Peripherals;
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);

/// Pipelined frames to present.
const FRAMES: u32 = 120;
/// VBlanks a queued flip is given before it counts as a timeout.
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]);
static mut FRAME: FrameStorage<8, [TriGouraud; TRIS]> = FrameStorage::new([EMPTY; TRIS]);

/// Wait until the queued flip is applied or `limit` VBlanks pass.
/// Returns whether it was applied.
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
}

/// Wait `n` VBlanks.
fn wait_vblanks(n: u32) {
    for _ in 0..n {
        interrupts::wait_vblank();
    }
}

/// Build frame `f`: `f % (TRIS + 1)` half-screen Gouraud triangles,
/// closed with GP0(1Fh) when `close` is set.
fn build_frame<'f>(
    frame: &mut OtFrame<'f, 8>,
    packets: &'f mut [TriGouraud; TRIS],
    f: u32,
    close: bool,
) {
    if close {
        frame.end_with_draw_done();
    }
    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) = Peripherals::take() else {
        return;
    };
    // The token moves into each in-flight frame and comes back from its
    // wait; between frames `Gpu::from_dma_mut` lends the driver.
    let display = DisplayConfig::new(VideoMode::Ntsc, Resolution::R320X240);
    let mut dma = Gpu::new(peripherals.gpu_dma, display).release();
    let mut fb = DoubleBuffer::new(Resolution::R320X240);
    fb.apply_draw_target(Gpu::from_dma_mut(&mut dma));
    // SAFETY: the only reference ever made to FRAME.
    let mut storage = unsafe { &mut *addr_of_mut!(FRAME) };

    // 1. Held: no GP0(1Fh), so the flip must stay queued even once the GPU
    // is idle.
    let gpu = Gpu::from_dma_mut(&mut dma);
    fb.clear(gpu, (8, 8, 24));
    gpu.arm_draw_done();
    let (in_flight, ()) = storage.draw_async(dma, |frame, packets| {
        build_frame(frame, packets, TRIS as u32, false)
    });
    interrupts::queue_display_control_at_vblank(fb.begin_deferred_swap());
    (storage, dma) = in_flight.wait();
    Gpu::from_dma_mut(&mut dma).wait_idle();
    wait_vblanks(4);
    let held = interrupts::is_display_control_queued() && !is_draw_done();

    // 2. Released: GP0(1Fh) through the port lets it land.
    Gpu::from_dma_mut(&mut dma).signal_draw_done();
    let released = wait_flip(4);
    if !released {
        let word = interrupts::take_queued_display_control();
        if word != 0 {
            psx_io::gpu::write_display_control(word);
        }
    }

    // 3. Pipelined frames through the queue.
    let mut timeouts = 0u32;
    let mut early = 0u32;
    let start = interrupts::vblank_count();
    for f in 0..FRAMES {
        let gpu = Gpu::from_dma_mut(&mut dma);
        fb.apply_draw_target(gpu);
        fb.clear(gpu, (8, 8, 24));
        gpu.arm_draw_done();
        let (in_flight, ()) =
            storage.draw_async(dma, |frame, packets| build_frame(frame, packets, f, true));
        interrupts::queue_display_control_at_vblank(fb.begin_deferred_swap());
        let flipped = wait_flip(WAIT_LIMIT);
        // A flip needs the frame drawn, so its walk is over and this wait
        // returns at once; after a timeout it ends the walk first.
        (storage, dma) = in_flight.wait();
        if !flipped {
            timeouts += 1;
            Gpu::from_dma_mut(&mut dma).wait_idle();
            let word = interrupts::take_queued_display_control();
            if word != 0 {
                psx_io::gpu::write_display_control(word);
            }
        } else if !is_draw_done() {
            // The flag cannot clear before the next arm, so a flip seen
            // without it happened before the frame's GP0(1Fh) ran.
            early += 1;
        }
    }
    let vblanks = interrupts::vblank_count().wrapping_sub(start);
    let gpu = Gpu::from_dma_mut(&mut dma);
    gpu.wait_idle();

    let checks = [
        (held, "flip without GP0(1Fh) was applied"),
        (released, "GP0(1Fh) did not release the flip"),
        (timeouts == 0, "queued flips timed out"),
        (early == 0, "flip before the frame's GP0(1Fh)"),
        (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 check failed: ");
        tty::println(what);
    }
    let lines = [
        line("failures", failures),
        line("frames", FRAMES),
        line("vblanks", vblanks),
        line("timeouts", timeouts),
        line("early", early),
    ];
    let (banner, tint) = if failures == 0 {
        ("PRESENT PASS", GREEN)
    } else {
        ("PRESENT FAIL", RED)
    };
    tty::print(banner);
    for (text, len) in &lines {
        tty::print(" ");
        tty::print(as_str(&text[..*len]));
    }
    tty::println("");

    let font = FontAtlas::upload(&BASIC, FONT_TPAGE, FONT_CLUT);
    loop {
        fb.apply_draw_target(gpu);
        fb.clear(gpu, (10, 12, 20));
        font.draw_text(8, 6, "QUEUED FLIP WAITS FOR GP0(1FH)", 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);
    }
}