Skip to main content
← PSoXide SDK guides and examples · SDK f2fec1c301f1 · All Cargo features enabled; check feature gates before use.

psx_telemetry/
emit.rs

1// SPDX-License-Identifier: GPL-2.0-or-later
2//! Guest-side telemetry event emitters (Expansion 2 MMIO ports).
3//!
4//! Every game carried a hand-copied shim of these writes (voxide and hl-psx
5//! byte-identical, oot-psx a subset); this module is that shim, once. The
6//! MMIO writes compile only with the crate's `emit` feature AND a MIPS
7//! target; games forward their own flag
8//! (`emulator-telemetry = ["psx-telemetry/emit"]`) so shipping builds pay
9//! only empty inlined calls.
10//!
11//! Ports (decoded by `emulator-core`, addresses in `psx_hw::memory::expansion2::telemetry`):
12//! the event word (`kind << 24 | id`), the value latch (written before the
13//! event that consumes it) and the debug-log byte stream.
14//!
15//! [`console`] is deliberately NOT feature-gated (mips-gated only): it is the
16//! Play debug terminal used from normal builds. Use sparingly.
17
18#[cfg(target_arch = "mips")]
19use psx_hw::memory::{expansion2::telemetry, to_kseg1};
20
21const EVENT_KIND_FRAME_BEGIN: u8 = 1;
22const EVENT_KIND_STAGE_BEGIN: u8 = 2;
23const EVENT_KIND_STAGE_END: u8 = 3;
24const EVENT_KIND_COUNTER: u8 = 4;
25const EVENT_KIND_TASK_BEGIN: u8 = 5;
26const EVENT_KIND_TASK_END: u8 = 6;
27
28/// The uncached view of one of the emulator's telemetry ports.
29#[cfg(target_arch = "mips")]
30const fn uncached(physical: u32) -> *mut u32 {
31    to_kseg1(physical) as *mut u32
32}
33
34#[cfg(all(target_arch = "mips", feature = "emit"))]
35const EVENT_ADDR: *mut u32 = uncached(telemetry::EVENT);
36#[cfg(all(target_arch = "mips", feature = "emit"))]
37const VALUE_ADDR: *mut u32 = uncached(telemetry::VALUE);
38#[cfg(all(target_arch = "mips", feature = "emit"))]
39const LOG_ADDR: *mut u32 = uncached(telemetry::LOG);
40
41/// Emulator cycle counter, wrapping at 32 bits. Returns zero on host builds
42/// and when the `emit` feature is disabled, without touching telemetry MMIO.
43#[inline(always)]
44pub fn cycles() -> u32 {
45    #[cfg(all(target_arch = "mips", feature = "emit"))]
46    {
47        // SAFETY: `telemetry::CYCLES` is the word-aligned cycle-counter port in the
48        // uncached Expansion 2 window, decoded by the emulator; the read has
49        // no side effects and touches no Rust-owned memory.
50        unsafe { core::ptr::read_volatile(uncached(telemetry::CYCLES) as *const u32) }
51    }
52    #[cfg(not(all(target_arch = "mips", feature = "emit")))]
53    {
54        0
55    }
56}
57
58/// Mark the start of guest frame `frame` (drives `--guest-frames` stops).
59#[inline(always)]
60pub fn frame_begin(frame: u32) {
61    emit_value(frame);
62    emit_event(EVENT_KIND_FRAME_BEGIN, 0);
63}
64
65/// Enter a profiling stage (see [`crate::stage`] for ids).
66#[inline(always)]
67pub fn stage_begin(stage_id: u16) {
68    emit_event(EVENT_KIND_STAGE_BEGIN, stage_id);
69}
70
71/// Leave a profiling stage.
72#[inline(always)]
73pub fn stage_end(stage_id: u16) {
74    emit_event(EVENT_KIND_STAGE_END, stage_id);
75}
76
77/// Record `value` under a counter id (see [`crate::counter`]).
78#[inline(always)]
79pub fn counter(counter_id: u16, value: u32) {
80    emit_value(value);
81    emit_event(EVENT_KIND_COUNTER, counter_id);
82}
83
84/// Enter a background-task span (see [`crate::task`]).
85#[inline(always)]
86pub fn task_begin(task_id: u16) {
87    emit_event(EVENT_KIND_TASK_BEGIN, task_id);
88}
89
90/// Leave a background-task span.
91#[inline(always)]
92pub fn task_end(task_id: u16) {
93    emit_event(EVENT_KIND_TASK_END, task_id);
94}
95
96/// Write a line to the emulator debug log, telemetry builds only.
97#[inline(always)]
98pub fn debug_log(message: &str) {
99    debug_bytes(message.as_bytes());
100    debug_byte(b'\n');
101}
102
103/// Write a line to the emulator's guest debug-log port UNCONDITIONALLY
104/// (mips-gated, not feature-gated), so it reaches PSoXide's Play debug
105/// terminal from a normal build. Use sparingly (debug tooling only).
106#[inline(always)]
107pub fn console(message: &str) {
108    #[cfg(target_arch = "mips")]
109    {
110        const PORT: *mut u32 = uncached(telemetry::LOG);
111        for &byte in message.as_bytes() {
112            // SAFETY: PORT is the word-aligned debug-log port in the uncached
113            // Expansion 2 window, not Rust-owned memory; each write only
114            // appends one byte to the emulator's log stream.
115            unsafe { core::ptr::write_volatile(PORT, byte as u32) };
116        }
117        // SAFETY: same debug-log port as the loop above.
118        unsafe { core::ptr::write_volatile(PORT, b'\n' as u32) };
119    }
120    #[cfg(not(target_arch = "mips"))]
121    {
122        let _ = message;
123    }
124}
125
126#[inline(always)]
127fn debug_bytes(bytes: &[u8]) {
128    for &byte in bytes {
129        debug_byte(byte);
130    }
131}
132
133#[cfg(any(all(target_arch = "mips", feature = "emit"), test))]
134#[inline(always)]
135fn encode_event(kind: u8, id: u16) -> u32 {
136    ((kind as u32) << 24) | id as u32
137}
138
139#[cfg(all(target_arch = "mips", feature = "emit"))]
140#[inline(always)]
141fn emit_value(value: u32) {
142    // SAFETY: VALUE_ADDR is the word-aligned value-latch port in the uncached
143    // Expansion 2 window, not Rust-owned memory; the write only latches a word.
144    unsafe {
145        core::ptr::write_volatile(VALUE_ADDR, value);
146    }
147}
148
149#[cfg(not(all(target_arch = "mips", feature = "emit")))]
150#[inline(always)]
151fn emit_value(_value: u32) {}
152
153#[cfg(all(target_arch = "mips", feature = "emit"))]
154#[inline(always)]
155fn debug_byte(byte: u8) {
156    // SAFETY: LOG_ADDR is the word-aligned debug-log port in the uncached
157    // Expansion 2 window, not Rust-owned memory; the write appends one byte.
158    unsafe {
159        core::ptr::write_volatile(LOG_ADDR, byte as u32);
160    }
161}
162
163#[cfg(not(all(target_arch = "mips", feature = "emit")))]
164#[inline(always)]
165fn debug_byte(_byte: u8) {}
166
167#[cfg(all(target_arch = "mips", feature = "emit"))]
168#[inline(always)]
169fn emit_event(kind: u8, id: u16) {
170    // SAFETY: EVENT_ADDR is the word-aligned event port in the uncached
171    // Expansion 2 window, not Rust-owned memory; the write records one event.
172    unsafe {
173        core::ptr::write_volatile(EVENT_ADDR, encode_event(kind, id));
174    }
175}
176
177#[cfg(not(all(target_arch = "mips", feature = "emit")))]
178#[inline(always)]
179fn emit_event(_kind: u8, _id: u16) {}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    /// The emulator decodes `kind = word >> 24` and `id = word & 0xFFFF`
186    /// (`emulator-core/src/telemetry.rs`: KIND_SHIFT 24, ID_MASK 0xFFFF) and
187    /// names kinds 1 to 6 FrameBegin, StageBegin, StageEnd, Counter,
188    /// TaskBegin, TaskEnd.
189    #[test]
190    fn event_words_decode_the_way_the_emulator_does() {
191        let kinds = [
192            (EVENT_KIND_FRAME_BEGIN, 1u8),
193            (EVENT_KIND_STAGE_BEGIN, 2),
194            (EVENT_KIND_STAGE_END, 3),
195            (EVENT_KIND_COUNTER, 4),
196            (EVENT_KIND_TASK_BEGIN, 5),
197            (EVENT_KIND_TASK_END, 6),
198        ];
199        for (kind, number) in kinds {
200            assert_eq!(kind, number);
201            for id in [0u16, 1, 51, 266, 0xFFFF] {
202                let word = encode_event(kind, id);
203                assert_eq!((word >> 24) & 0xFF, number as u32);
204                assert_eq!(word & 0xFFFF, id as u32);
205            }
206        }
207    }
208
209    #[test]
210    fn emitters_are_silent_no_ops_off_target() {
211        // Host builds and builds without `emit` touch no port and read no
212        // cycle counter.
213        assert_eq!(cycles(), 0);
214        frame_begin(7);
215        stage_begin(crate::stage::UPDATE);
216        stage_end(crate::stage::UPDATE);
217        counter(crate::counter::TRI_PRIMITIVES, 9);
218        task_begin(crate::task::FIXED_UPDATE);
219        task_end(crate::task::FIXED_UPDATE);
220        debug_log("x");
221        console("x");
222    }
223}