What this program does
Poll port 1 and display the controller state. The D-pad changes the background, while face buttons draw coloured triangles. Read the state display when checking digital versus analog mode; controller capabilities can differ.
PS1 homebrew, running in your browser. This player runs the compiled PS1 executable in the PSoXide emulator.
Focus the game to use the keyboard. Arrows: D-pad. F / G / H / X: Cross / Circle / Square / Triangle. Enter: Start. Sound starts muted; use the speaker button in the game. No touch controls.
Open player separately · Download EXE · Matching source · Builds and credits
Build and run
Use the documented SDK checkout and tool setup. Run these commands from the repository root.
make disc EXAMPLE=hello-input
Open build/examples/mipsel-sony-psx/release/hello-input.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-pad, psx-rt, psx-vram.
Cargo.toml · 23 lines
[package]
name = "hello-input"
version = "0.1.0"
edition = "2021"
license = "GPL-2.0-or-later"
[workspace]
[[bin]]
name = "hello-input"
path = "src/main.rs"
[profile.release]
panic = "abort"
codegen-units = 1
lto = true
[dependencies]
psx-gpu = { path = "../../crates/psx-gpu" }
psx-pad = { path = "../../crates/psx-pad" }
psx-rt = { path = "../../crates/psx-rt" }
psx-font = { path = "../../crates/psx-font" }
psx-vram = { path = "../../crates/psx-vram" }
src/main.rs · 299 lines
//! `hello-input` -- poll the port-1 pad every frame and
//! paint the screen with feedback that reacts to the user's input.
//!
//! Proves two pipelines end-to-end:
//! 1. SIO0 + pad (keyboard event → winit → emulator bus → SIO0 pad
//! → homebrew poll).
//! 2. `psx-font`: ASCII text rendering through a 4bpp CLUT atlas
//! in VRAM. The label row at the top spells out every held button
//! so the visual matches whatever we polled.
//!
//! Bindings (mapped by the frontend's keyboard handler):
//! - D-pad UP → brighten red channel
//! - D-pad DOWN → brighten green channel
//! - D-pad LEFT → brighten blue channel
//! - D-pad RIGHT → reset to black
//! - CROSS / CIRCLE / TRIANGLE / SQUARE draw a small coloured
//! triangle in the centre whose orientation rotates with the
//! face-button pressed.
//! - The program asks for analog mode at boot and locks it
//! (`require_analog_on`, see `sdk/docs/PAD-ANALOG.md`), so the
//! DualShock Analog button (F9 on the frontend keyboard, or gamepad
//! Mode/Guide when the OS exposes it) no longer switches it back.
//! A pad plugged in later is asked again. In analog mode the
//! bottom-left status panel shows raw stick bytes. Keyboard
//! defaults: arrow keys for left stick, I/J/K/L for right stick.
#![no_std]
#![no_main]
extern crate psx_rt;
use psx_font::{fonts::BASIC, FontAtlas};
use psx_gpu::display::{DisplayConfig, DoubleBuffer, Resolution, VideoMode};
use psx_gpu::prim::TriFlat;
use psx_gpu::Gpu;
use psx_pad::{button, require_analog_on, ButtonState, PadMode, PadReader, PadState, Port};
use psx_vram::{Clut, TextureDepth, TexturePage};
/// Font atlas VRAM slot.
///
/// With double-buffering, buffer A is at (0..320, 0..240) and B
/// at (0..320, 240..480). TexturePage X must be a multiple of 64 → 320
/// is the lowest slot that clears both buffers. At 4bpp, a BASIC
/// 128-glyph atlas uses 64 halfwords × 32 halfword rows →
/// (320..384) × (0..32), inside tpage (320, 0, Bit4).
const FONT_TPAGE: TexturePage = TexturePage::new(320, 0, TextureDepth::Bit4);
/// 2-entry CLUT (transparent + white) at (320, 256). X is a
/// multiple of 16 ✓, past the right edge of buffer B at X=320 ✓.
const FONT_CLUT: Clut = Clut::new(320, 256);
#[no_mangle]
fn main() {
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));
// One-shot: expand the 1bpp BASIC font into a 4bpp CLUT texture
// + a tiny transparent/white CLUT, both parked off the
// framebuffer.
let font = FontAtlas::upload(&BASIC, FONT_TPAGE, FONT_CLUT);
let mut r: u8 = 0;
let mut g: u8 = 0;
let mut b: u8 = 32;
// A program started by a launcher inherits whatever mode the last one
// left, so it asks for analog and the lock itself. The reader keeps the
// last clean state through a poll the driver had to reject.
let mut port = peripherals.controller_port;
let _ = require_analog_on(&mut port, Port::One);
let mut reader = PadReader::port1();
let mut was_connected = true;
loop {
let state = reader.poll_on(&mut port);
// A pad plugged in later starts in digital mode, and one parked in
// configuration mode (ID 0xF3) never reaches analog by itself: ask
// again for both, not every frame for a digital-only pad (the
// request blocks for a few frames).
if (state.is_connected() && !was_connected) || state.mode == PadMode::Config {
let _ = require_analog_on(&mut port, Port::One);
}
was_connected = state.is_connected();
let pad = state.buttons;
// Direction buttons nudge the background color.
if pad.is_held(button::UP) {
r = r.saturating_add(4);
}
if pad.is_held(button::DOWN) {
g = g.saturating_add(4);
}
if pad.is_held(button::LEFT) {
b = b.saturating_add(4);
}
if pad.is_held(button::RIGHT) {
r = 0;
g = 0;
b = 32;
}
// Background -- clear the back buffer.
fb.clear(&mut gpu, (r, g, b));
// Face buttons paint a coloured triangle in the centre of
// the screen. Each direction rotates 90° so "Triangle"
// literally points up, etc.
face_button_tri(&mut gpu, pad);
// Label row: show every held button as its uppercase name.
// `(0x80, 0x80, 0x80)` = unmodulated white (PSX texture tint
// is `output = texel * tint / 128`).
draw_button_labels(&font, pad);
draw_pad_status(&font, state);
gpu.wait_idle();
psx_rt::interrupts::wait_vblank();
fb.swap(&mut gpu);
}
}
/// Paint the name of every held button down the top-left of the
/// screen. One line per button -- 8 px tall, so 14 buttons fits
/// comfortably inside 240 px of vertical room with a 2px gap.
fn draw_button_labels(font: &FontAtlas, pad: ButtonState) {
// Header, unconditional -- sanity check that the font is alive
// even when no key is pressed.
font.draw_text(4, 4, "HELD:", (0x80, 0x80, 0x80));
// (button mask, label, tint) -- tints match the triangle colours
// where applicable so the label lines up visually with the
// on-screen primitive.
let rows: &[(u16, &str, (u8, u8, u8))] = &[
(button::UP, "UP", (0xC0, 0x30, 0x30)),
(button::DOWN, "DOWN", (0x30, 0xC0, 0x30)),
(button::LEFT, "LEFT", (0x30, 0x30, 0xC0)),
(button::RIGHT, "RIGHT", (0xA0, 0xA0, 0xA0)),
(button::TRIANGLE, "TRIANGLE", (0x30, 0xC0, 0x30)),
(button::CIRCLE, "CIRCLE", (0xC0, 0x30, 0x30)),
(button::CROSS, "CROSS", (0x30, 0x80, 0xC0)),
(button::SQUARE, "SQUARE", (0xC0, 0x30, 0xC0)),
(button::L1, "L1", (0xA0, 0xA0, 0x30)),
(button::R1, "R1", (0xA0, 0xA0, 0x30)),
(button::L2, "L2", (0x60, 0xA0, 0x60)),
(button::R2, "R2", (0x60, 0xA0, 0x60)),
(button::L3, "L3", (0x60, 0xC0, 0xC0)),
(button::R3, "R3", (0x60, 0xC0, 0xC0)),
(button::START, "START", (0xE0, 0xE0, 0xE0)),
(button::SELECT, "SELECT", (0xE0, 0xE0, 0xE0)),
];
// Label area: start at y=16 (below the HELD: header), advance 10
// px per line (8 px glyph + 2 px gap). Skip unpressed rows.
let mut y: i16 = 16;
for &(mask, label, tint) in rows {
if pad.is_held(mask) {
font.draw_text(4, y, label, tint);
y = y.wrapping_add(10);
}
}
// Bottom-right: show the raw bitmask in hex so the parity tests
// can latch onto something stable regardless of which buttons
// were held.
let hex = hex_u16(pad.bits());
font.draw_text(320 - 8 * 6 - 4, 240 - 10, &hex, (0x70, 0x70, 0x70));
}
fn draw_pad_status(font: &FontAtlas, state: PadState) {
let (mode, tint) = match state.mode {
PadMode::Disconnected => ("NO PAD", (0xE0, 0x50, 0x50)),
PadMode::Digital => ("DIGITAL", (0xE0, 0x90, 0x40)),
PadMode::Analog => ("ANALOG", (0x60, 0xE0, 0x80)),
PadMode::Config => ("CONFIG", (0x90, 0xA0, 0xE0)),
PadMode::Unknown => ("UNKNOWN", (0xE0, 0xE0, 0x60)),
};
font.draw_text(4, 188, "MODE:", (0x80, 0x80, 0x80));
font.draw_text(52, 188, mode, tint);
let id = hex_u8(state.id_low);
font.draw_text(124, 188, "ID:", (0x60, 0x60, 0x60));
font.draw_text(152, 188, &id, (0x70, 0x70, 0x70));
font.draw_text(4, 200, "ANALOG:", (0x80, 0x80, 0x80));
if state.is_analog() {
font.draw_text(68, 200, "ON", (0x60, 0xE0, 0x80));
let lx = hex_u8(state.sticks.left_x);
let ly = hex_u8(state.sticks.left_y);
let rx = hex_u8(state.sticks.right_x);
let ry = hex_u8(state.sticks.right_y);
font.draw_text(4, 212, "L:", (0x80, 0x80, 0x80));
font.draw_text(28, 212, &lx, (0x70, 0xC0, 0xE0));
font.draw_text(68, 212, &ly, (0x70, 0xC0, 0xE0));
font.draw_text(4, 224, "R:", (0x80, 0x80, 0x80));
font.draw_text(28, 224, &rx, (0xC0, 0x90, 0xE0));
font.draw_text(68, 224, &ry, (0xC0, 0x90, 0xE0));
} else if state.is_connected() {
font.draw_text(68, 200, "OFF", (0xE0, 0x90, 0x40));
font.draw_text(4, 212, "PRESS ANALOG BUTTON", (0x90, 0x90, 0x90));
} else {
font.draw_text(68, 200, "N/A", (0xE0, 0x50, 0x50));
font.draw_text(4, 212, "CONNECT A CONTROLLER", (0xE0, 0x90, 0x40));
}
}
/// `0x1234` as a 6-char string -- `'0'`, `'x'`, then 4 uppercase hex
/// digits, written into a stack buffer. no_std-friendly (no alloc).
fn hex_u16(v: u16) -> HexU16 {
const HEX: &[u8; 16] = b"0123456789ABCDEF";
let mut out = [0u8; 6];
out[0] = b'0';
out[1] = b'x';
out[2] = HEX[((v >> 12) & 0xF) as usize];
out[3] = HEX[((v >> 8) & 0xF) as usize];
out[4] = HEX[((v >> 4) & 0xF) as usize];
out[5] = HEX[(v & 0xF) as usize];
HexU16(out)
}
/// `0x12` as a 4-char string.
fn hex_u8(v: u8) -> HexU8 {
const HEX: &[u8; 16] = b"0123456789ABCDEF";
let mut out = [0u8; 4];
out[0] = b'0';
out[1] = b'x';
out[2] = HEX[((v >> 4) & 0xF) as usize];
out[3] = HEX[(v & 0xF) as usize];
HexU8(out)
}
/// Owning newtype around a 4-byte hex buffer.
struct HexU8([u8; 4]);
impl HexU8 {
fn as_str(&self) -> &str {
// SAFETY: only ASCII hex digits + '0', 'x' are ever written
// to the buffer.
unsafe { core::str::from_utf8_unchecked(&self.0) }
}
}
impl core::ops::Deref for HexU8 {
type Target = str;
fn deref(&self) -> &str {
self.as_str()
}
}
/// Owning newtype around a 6-byte hex buffer so `as_str` borrows
/// from `self` -- avoids dangling-slice pitfalls with stack arrays.
struct HexU16([u8; 6]);
impl HexU16 {
fn as_str(&self) -> &str {
// SAFETY: only ASCII hex digits + '0', 'x' are ever written
// to the buffer.
unsafe { core::str::from_utf8_unchecked(&self.0) }
}
}
impl core::ops::Deref for HexU16 {
type Target = str;
fn deref(&self) -> &str {
self.as_str()
}
}
fn face_button_tri(gpu: &mut Gpu, pad: ButtonState) {
let (verts, color) = if pad.is_held(button::TRIANGLE) {
(
[(160, 100), (140, 140), (180, 140)],
(80, 220, 80), // green
)
} else if pad.is_held(button::CIRCLE) {
(
[(180, 120), (140, 100), (140, 140)],
(220, 80, 80), // red
)
} else if pad.is_held(button::CROSS) {
(
[(160, 140), (140, 100), (180, 100)],
(80, 160, 220), // blue
)
} else if pad.is_held(button::SQUARE) {
(
[(140, 120), (180, 100), (180, 140)],
(220, 80, 220), // magenta
)
} else {
// No face button held: tiny white triangle so the app still
// shows signs of life.
([(160, 118), (156, 124), (164, 124)], (200, 200, 200))
};
gpu.draw(&TriFlat::new(verts, color.0, color.1, color.2));
}