SDK example · full source

cdda-read-contention

CD data commands during audio playback

On this page

What this program does

Start CD-DA playback, then issue a data-read command and record the drive response. This intentionally probes command contention. It is not the recommended streaming pattern for a game and needs a disc with track 2 audio.

Build and run

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

Build the executable, then master a mixed-mode image with your own sector-aligned raw CD-DA audio. Replace /path/to/track.raw with a prepared audio file; the packer does not convert a WAV or MP3 in this command.

make example EXAMPLE=cdda-read-contention
cargo run --locked --release -p mkisopsx -- \
  --exe build/examples/mipsel-sony-psx/release/cdda-read-contention.exe \
  --out build/examples/mipsel-sony-psx/release/cdda-read-contention.bin \
  --cdda-track /path/to/track.raw

Open the resulting CUE with the BIN beside it. A generic make disc image has no audio track. See the audio cooker's source and formats when preparing your source audio.

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

Cargo.toml · 21 lines

View this file on GitHub

[package]
name    = "cdda-read-contention"
version = "0.1.0"
edition = "2021"
license = "GPL-2.0-or-later"

[workspace]

[[bin]]
name = "cdda-read-contention"
path = "src/main.rs"

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

[dependencies]
psx-io = { path = "../../crates/psx-io" }
psx-hw = { path = "../../../crates/psx-hw" }
psx-rt = { path = "../../crates/psx-rt" }
src/main.rs · 236 lines

View this file on GitHub

//! `cdda-read-contention` -- a focused PS1 CD-ROM conformance probe.
//!
//! It reproduces the exact situation cortex_ignition_v1 hits on real
//! hardware: menu CD-DA is playing, then gameplay issues a data read
//! WITHOUT pausing/stopping the drive first. The guest's `cd_stream`
//! reports `STATUS_CD_ERROR` there, which is an INT5 (Error) IRQ.
//!
//! This program drives the controller through the engine's real read
//! path and records WHICH IRQ the ReadN produces, so the same binary can
//! be compared across PSoXide, PCSX-Redux, DuckStation, and a console.
//!
//! Sequence (matches `engine/.../cd_stream/hw.rs`):
//!   SetMode CDDA|2x -> Demute -> Play(track 2)        [CD-DA, PLAYING]
//!   SetMode 2x (drop CDDA) -> SetLoc(data LBA) -> ReadN    [contention]
//!
//! Result is written to uncached RAM at `RESULT_BASE` (so an emulator
//! peek or a hardware probe sees it without cache games) and printed to
//! TTY. `RESULT_BASE+0` is the completion magic, written last.

#![no_std]
#![no_main]

extern crate psx_rt;

use psx_io::{read_u8, write_u32, write_u8};
use psx_rt::tty;

use psx_hw::cd::index_status::{
    DATA_FIFO_NOT_EMPTY as STAT_DATA_NOT_EMPTY, RESPONSE_NOT_EMPTY as STAT_RESP_NOT_EMPTY,
};
use psx_hw::cd::irq::{DATA_READY as IRQ_DATA_READY, ERROR as IRQ_ERROR};
use psx_hw::cd::reg::{
    COMMAND_RESPONSE as CD_CMD, INDEX as CD_STATUS, PARAMETER as CD_PARAM, REQUEST_IRQ as CD_IRQ,
};

// Uncached (KSEG1) RAM mirror so writes land in RAM immediately.
const RESULT_BASE: u32 = 0xA010_0000;
const DONE_MAGIC: u32 = 0x0C0D_0001;

const PLAY_TRACK: u8 = 2;
const DATA_LBA: u32 = 16;
const POLL_LIMIT: u32 = 4_000_000;

static SPIN_SINK: u32 = 0;

struct Resp {
    irq: u8,
    stat: u8,
}

#[no_mangle]
fn main() {
    unsafe {
        run();
    }
    loop {
        core::hint::spin_loop();
    }
}

unsafe fn run() {
    // Mask CDROM at the CPU IRQ controller and poll the controller flag
    // directly, exactly like the engine's polled read path.
    psx_io::irq::set_mask(1 << psx_hw::irq::source::VBLANK);
    cd_set_index(1);
    write_u8(CD_PARAM, 0x00); // controller IRQ enable = 0 (poll only)
    write_u8(CD_IRQ, 0x1F); // ack any pending
    cd_set_index(0);

    write_result(0x14, 1);

    // 1) Menu-style CD-DA playback.
    let _ = cd_command(0x0E, &[0x81]); // SetMode: CDDA | DOUBLE_SPEED
    let _ = cd_command(0x0C, &[]); // Demute
    let _ = cd_command(0x03, &[bcd(PLAY_TRACK)]); // Play track 2

    // Let the drive settle into playback, then sample the stat byte.
    spin(2_000_000);
    let play = cd_command(0x01, &[]); // GetStat
    write_result(0x08, play.stat as u32);
    write_result(0x14, 2);

    // 2) Engine read path WHILE CD-DA plays, with no Pause/Stop first.
    let _ = cd_command(0x0E, &[0x80]); // SetMode: DOUBLE_SPEED only (drop CDDA)
    let (m, s, f) = lba_to_bcd_msf(DATA_LBA);
    let _ = cd_command(0x02, &[m, s, f]); // SetLoc(data sector)
    write_result(0x14, 3);

    // Issue ReadN and capture EVERY IRQ it produces.
    cd_ack_all();
    cd_set_index(0);
    drain_responses();
    write_u8(CD_CMD, 0x06); // ReadN

    let mut irq_seen = 0u32;
    let mut last_stat = 0u8;
    let mut data_ready = 0u32;
    let mut polls = 0;
    while polls < 6 {
        let flag = poll_irq(POLL_LIMIT);
        if flag == 0 {
            break;
        }
        irq_seen |= 1u32 << (flag & 0x7);
        last_stat = read_first_response();
        if cd_status() & STAT_DATA_NOT_EMPTY != 0 {
            data_ready = 1;
        }
        cd_ack(flag);
        // A data-ready or an error settles the question.
        if flag == IRQ_DATA_READY || flag == IRQ_ERROR {
            break;
        }
        polls += 1;
    }
    if cd_status() & STAT_DATA_NOT_EMPTY != 0 {
        data_ready = 1;
    }

    write_result(0x04, irq_seen);
    write_result(0x0C, last_stat as u32);
    write_result(0x10, data_ready);
    write_result(0x14, 4);

    // Human/harness-readable summary.
    tty::print("CDDA-CONTENTION irq=0x");
    tty::print_hex_u32(irq_seen);
    tty::print(" play_stat=0x");
    tty::print_hex_u32(play.stat as u32);
    tty::print(" read_stat=0x");
    tty::print_hex_u32(last_stat as u32);
    tty::print(" data=0x");
    tty::print_hex_u32(data_ready);
    if irq_seen & (1 << IRQ_ERROR) != 0 {
        tty::println(" RESULT=INT5_ERROR");
    } else if irq_seen & (1 << IRQ_DATA_READY) != 0 {
        tty::println(" RESULT=DATA_DELIVERED");
    } else {
        tty::println(" RESULT=UNKNOWN");
    }

    // Completion marker LAST so a reader can poll RESULT_BASE+0.
    write_result(0x00, DONE_MAGIC);
}

unsafe fn cd_command(cmd: u8, params: &[u8]) -> Resp {
    cd_ack_all();
    cd_set_index(0);
    drain_responses();
    for &p in params {
        write_u8(CD_PARAM, p);
    }
    write_u8(CD_CMD, cmd);
    let irq = poll_irq(POLL_LIMIT); // INT3 ack, or INT5 error
    let stat = read_first_response();
    cd_ack(irq);
    Resp { irq, stat }
}

unsafe fn poll_irq(limit: u32) -> u8 {
    let mut i = 0u32;
    loop {
        cd_set_index(1);
        let flag = read_u8(CD_IRQ) & 0x1F;
        cd_set_index(0);
        if flag != 0 {
            return flag;
        }
        if i >= limit {
            return 0;
        }
        i += 1;
    }
}

unsafe fn read_first_response() -> u8 {
    cd_set_index(0);
    let mut first = 0u8;
    let mut got = false;
    while cd_status() & STAT_RESP_NOT_EMPTY != 0 {
        let b = read_u8(CD_CMD);
        if !got {
            first = b;
            got = true;
        }
    }
    first
}

unsafe fn drain_responses() {
    let _ = read_first_response();
}

unsafe fn cd_ack(bits: u8) {
    cd_set_index(1);
    write_u8(CD_IRQ, bits & 0x1F);
    cd_set_index(0);
}

unsafe fn cd_ack_all() {
    cd_ack(0x1F);
}

unsafe fn cd_status() -> u8 {
    read_u8(CD_STATUS)
}

unsafe fn cd_set_index(index: u8) {
    write_u8(CD_STATUS, index & 0x03);
}

unsafe fn write_result(offset: u32, value: u32) {
    write_u32(RESULT_BASE + offset, value);
}

fn bcd(v: u8) -> u8 {
    ((v / 10) << 4) | (v % 10)
}

fn lba_to_bcd_msf(lba: u32) -> (u8, u8, u8) {
    let total = lba + 150;
    let m = (total / (75 * 60)) as u8;
    let s = ((total / 75) % 60) as u8;
    let f = (total % 75) as u8;
    (bcd(m), bcd(s), bcd(f))
}

fn spin(n: u32) {
    let mut i = 0u32;
    while i < n {
        unsafe {
            core::ptr::read_volatile(&SPIN_SINK);
        }
        i += 1;
    }
}