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

psx_math/
fmt.rs

1// SPDX-License-Identifier: GPL-2.0-or-later
2//! Integer-to-decimal ASCII without `core::fmt`.
3//!
4//! `core::fmt` drags kilobytes of formatting machinery into a guest binary,
5//! so every project that draws a score, a coordinate, or a cell value with
6//! `psx-font` grew its own digit loop (gh-psx `u32_dec`, PSXcel `write_dec` /
7//! `write_u64`, hl-psx `fmt_i32`, VoXide `Decimal3`, the magikarp example).
8//! This module is that loop, once.
9//!
10//! Each function renders right-to-left into a scratch tail of `buf`, then
11//! returns the `&str` covering just the digits. Size `buf` with the matching
12//! `*_DEC_MAX` constant and it can never fail; an undersized buffer panics
13//! (slice bounds), which the guest panic handler reports with file:line.
14
15/// Maximum bytes `u32_dec` can produce (`u32::MAX` = 10 digits).
16pub const U32_DEC_MAX: usize = 10;
17/// Maximum bytes `i32_dec` can produce (sign + 10 digits).
18pub const I32_DEC_MAX: usize = 11;
19/// Maximum bytes `u64_dec` can produce (`u64::MAX` = 20 digits).
20pub const U64_DEC_MAX: usize = 20;
21
22/// The formatted bytes as `&str`. Only this module's writers reach it, and
23/// they write ASCII digits and `-` alone, so UTF-8 validation is skipped: it
24/// was 280 of the 500 cycles a 32-bit `u32_dec` call took on the emulator
25/// microbench (the old `u64` route took 757).
26#[inline(always)]
27fn ascii(bytes: &[u8]) -> &str {
28    debug_assert!(bytes.is_ascii());
29    // SAFETY: every byte is an ASCII digit or '-', so the slice is UTF-8.
30    unsafe { core::str::from_utf8_unchecked(bytes) }
31}
32
33/// Format `v` as decimal ASCII into `buf`, returning the digits as `&str`.
34///
35/// `buf` must be at least [`U32_DEC_MAX`] bytes for the general case.
36/// 32-bit only: the constant `/ 10` becomes a `multu` by its reciprocal and
37/// the digit comes from `v - q * 10`, so no divide and no 64-bit helper runs.
38pub fn u32_dec(buf: &mut [u8], mut v: u32) -> &str {
39    let mut tmp = [0u8; U32_DEC_MAX];
40    let mut i = tmp.len();
41    loop {
42        let q = v / 10;
43        i -= 1;
44        tmp[i] = b'0' + (v - q * 10) as u8;
45        v = q;
46        if v == 0 {
47            break;
48        }
49    }
50    let n = tmp.len() - i;
51    buf[..n].copy_from_slice(&tmp[i..]);
52    ascii(&buf[..n])
53}
54
55/// Format `v` as decimal ASCII into `buf`, returning the digits as `&str`.
56///
57/// `buf` must be at least [`I32_DEC_MAX`] bytes for the general case.
58/// Handles `i32::MIN` (no negate overflow: magnitude goes through u32).
59pub fn i32_dec(buf: &mut [u8], v: i32) -> &str {
60    if v >= 0 {
61        return u32_dec(buf, v as u32);
62    }
63    // Sign first, magnitude right after it (u32_dec writes from the start
64    // of the slice it is given).
65    buf[0] = b'-';
66    let n = 1 + u32_dec(&mut buf[1..], v.unsigned_abs()).len();
67    ascii(&buf[..n])
68}
69
70/// Format `v` as decimal ASCII into `buf`, returning the digits as `&str`.
71///
72/// `buf` must be at least [`U64_DEC_MAX`] bytes for the general case.
73/// (64-bit math is slow on the PS1; fine for text, keep it out of hot loops.)
74pub fn u64_dec(buf: &mut [u8], mut v: u64) -> &str {
75    let mut tmp = [0u8; U64_DEC_MAX];
76    let mut i = tmp.len();
77    loop {
78        i -= 1;
79        tmp[i] = b'0' + (v % 10) as u8;
80        v /= 10;
81        if v == 0 {
82            break;
83        }
84    }
85    let n = tmp.len() - i;
86    buf[..n].copy_from_slice(&tmp[i..]);
87    ascii(&buf[..n])
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn u32_basics() {
96        let mut b = [0u8; U32_DEC_MAX];
97        assert_eq!(u32_dec(&mut b, 0), "0");
98        assert_eq!(u32_dec(&mut b, 7), "7");
99        assert_eq!(u32_dec(&mut b, 240), "240");
100        assert_eq!(u32_dec(&mut b, u32::MAX), "4294967295");
101    }
102
103    #[test]
104    fn i32_signs_and_extremes() {
105        let mut b = [0u8; I32_DEC_MAX];
106        assert_eq!(i32_dec(&mut b, 0), "0");
107        assert_eq!(i32_dec(&mut b, -1), "-1");
108        assert_eq!(i32_dec(&mut b, 12345), "12345");
109        assert_eq!(i32_dec(&mut b, i32::MIN), "-2147483648");
110        assert_eq!(i32_dec(&mut b, i32::MAX), "2147483647");
111    }
112
113    /// `u32_dec` against the 64-bit formatter it replaced, at every digit-count
114    /// boundary and on random values.
115    #[test]
116    #[cfg_attr(
117        miri,
118        ignore = "exhaustive sweep, minutes under Miri; make test runs it natively"
119    )]
120    fn u32_matches_u64_formatter() {
121        let check = |v: u32| {
122            let mut a = [0u8; U32_DEC_MAX];
123            let mut b = [0u8; U64_DEC_MAX];
124            assert_eq!(u32_dec(&mut a, v), u64_dec(&mut b, u64::from(v)), "{v}");
125        };
126        let mut power = 1u32;
127        loop {
128            for v in [
129                power - 1,
130                power,
131                power + 1,
132                power.wrapping_mul(2) - 1,
133                power.wrapping_mul(9),
134            ] {
135                check(v);
136            }
137            match power.checked_mul(10) {
138                Some(next) => power = next,
139                None => break,
140            }
141        }
142        for v in (0..100_000).chain(u32::MAX - 100_000..=u32::MAX) {
143            check(v);
144        }
145        let mut seed = 0x1234_5678u32;
146        for _ in 0..1_000_000 {
147            seed ^= seed << 13;
148            seed ^= seed >> 17;
149            seed ^= seed << 5;
150            check(seed);
151            check(seed >> (seed % 32));
152        }
153    }
154
155    /// Every u32, against the 64-bit formatter. Several minutes in a debug
156    /// build: `cargo test --release -p psx-math -- --ignored`.
157    #[test]
158    #[ignore]
159    fn u32_matches_u64_formatter_exhaustively() {
160        let mut a = [0u8; U32_DEC_MAX];
161        let mut b = [0u8; U64_DEC_MAX];
162        for v in 0..=u32::MAX {
163            assert_eq!(u32_dec(&mut a, v), u64_dec(&mut b, u64::from(v)), "{v}");
164        }
165    }
166
167    #[test]
168    #[cfg_attr(
169        miri,
170        ignore = "exhaustive sweep, minutes under Miri; make test runs it natively"
171    )]
172    fn i32_matches_u64_formatter() {
173        let mut seed = 0x9e37_79b9u32;
174        for _ in 0..200_000 {
175            seed ^= seed << 13;
176            seed ^= seed >> 17;
177            seed ^= seed << 5;
178            let v = seed as i32 >> (seed % 32);
179            let mut a = [0u8; I32_DEC_MAX];
180            let mut b = [0u8; U64_DEC_MAX];
181            let magnitude = u64_dec(&mut b, u64::from(v.unsigned_abs()));
182            let sign = if v < 0 { "-" } else { "" };
183            assert_eq!(i32_dec(&mut a, v), [sign, magnitude].concat(), "{v}");
184        }
185    }
186
187    #[test]
188    fn u64_extremes() {
189        let mut b = [0u8; U64_DEC_MAX];
190        assert_eq!(u64_dec(&mut b, u64::MAX), "18446744073709551615");
191        assert_eq!(u64_dec(&mut b, 10_000), "10000");
192    }
193}