Showing preview only (322K chars total). Download the full file or copy to clipboard to get everything.
Repository: spicyjpeg/ps1-bare-metal
Branch: main
Commit: 84b401c2cc11
Files: 63
Total size: 303.9 KB
Directory structure:
gitextract_wxeqi4ti/
├── .clangd
├── .editorconfig
├── .github/
│ ├── scripts/
│ │ └── buildToolchain.sh
│ └── workflows/
│ └── build.yml
├── .gitignore
├── .markdownlint.json
├── CMakeLists.txt
├── CMakePresets.json
├── LICENSE
├── README.md
├── cmake/
│ ├── executable.ld
│ ├── setup.cmake
│ ├── toolchain.cmake
│ └── tools.cmake
├── src/
│ ├── 00_helloWorld/
│ │ └── main.c
│ ├── 01_basicGraphics/
│ │ └── main.c
│ ├── 02_doubleBuffer/
│ │ └── main.c
│ ├── 03_dmaChain/
│ │ └── main.c
│ ├── 04_textures/
│ │ └── main.c
│ ├── 05_palettes/
│ │ └── main.c
│ ├── 06_fonts/
│ │ ├── gpu.c
│ │ ├── gpu.h
│ │ └── main.c
│ ├── 07_orderingTable/
│ │ ├── gpu.c
│ │ ├── gpu.h
│ │ └── main.c
│ ├── 08_spinningCube/
│ │ ├── gpu.c
│ │ ├── gpu.h
│ │ ├── main.c
│ │ ├── trig.c
│ │ └── trig.h
│ ├── 09_controllers/
│ │ ├── font.c
│ │ ├── font.h
│ │ ├── gpu.c
│ │ ├── gpu.h
│ │ └── main.c
│ ├── libc/
│ │ ├── assert.h
│ │ ├── clz.s
│ │ ├── crt0.c
│ │ ├── ctype.h
│ │ ├── cxxsupport.cpp
│ │ ├── malloc.c
│ │ ├── misc.c
│ │ ├── setjmp.h
│ │ ├── setjmp.s
│ │ ├── stdio.h
│ │ ├── stdlib.h
│ │ ├── string.c
│ │ ├── string.h
│ │ └── string.s
│ ├── ps1/
│ │ ├── cache.h
│ │ ├── cache.s
│ │ ├── cdrom.h
│ │ ├── cop0.h
│ │ ├── gpucmd.h
│ │ ├── gte.h
│ │ └── registers.h
│ └── vendor/
│ ├── LICENSE.printf
│ ├── printf.c
│ └── printf.h
└── tools/
├── convertExecutable.py
├── convertImage.py
└── requirements.txt
================================================
FILE CONTENTS
================================================
================================================
FILE: .clangd
================================================
# As clang/clangd's MIPS-I support is still experimental, some minor changes to
# the GCC arguments it picks up from CMake are required in order to prevent it
# from erroring out. Additionally, specifying the target architecture manually
# fixes some edge cases (such as CMake emitting 8.3 format paths on Windows and
# breaking clangd's target autodetection).
CompileFlags:
Add: [ --target=mipsel-none-elf, -march=mips1 ]
Remove: [ -march, -mno-llsc, -mdivide-breaks ]
================================================
FILE: .editorconfig
================================================
root = true
[*]
indent_style = tab
indent_size = 4
charset = utf-8
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
[{*.yml,*.yaml,.clangd}]
indent_style = space
indent_size = 2
================================================
FILE: .github/scripts/buildToolchain.sh
================================================
#!/bin/bash
ROOT_DIR="$(pwd)"
BINUTILS_VERSION="2.43"
GCC_VERSION="14.2.0"
NUM_JOBS="4"
if [ $# -eq 2 ]; then
PACKAGE_NAME="$1"
TARGET_NAME="$2"
BUILD_OPTIONS=""
elif [ $# -eq 3 ]; then
PACKAGE_NAME="$1"
TARGET_NAME="$2"
BUILD_OPTIONS="--build=x86_64-linux-gnu --host=$3"
else
echo "Usage: $0 <package name> <target triplet> [host triplet]"
exit 0
fi
## Download binutils and GCC
if [ ! -d binutils-$BINUTILS_VERSION ]; then
wget "https://ftpmirror.gnu.org/gnu/binutils/binutils-$BINUTILS_VERSION.tar.xz" \
|| exit 1
tar Jxf binutils-$BINUTILS_VERSION.tar.xz \
|| exit 1
rm -f binutils-$BINUTILS_VERSION.tar.xz
fi
if [ ! -d gcc-$GCC_VERSION ]; then
wget "https://ftpmirror.gnu.org/gnu/gcc/gcc-$GCC_VERSION/gcc-$GCC_VERSION.tar.xz" \
|| exit 1
tar Jxf gcc-$GCC_VERSION.tar.xz \
|| exit 1
cd gcc-$GCC_VERSION
contrib/download_prerequisites \
|| exit 1
cd ..
rm -f gcc-$GCC_VERSION.tar.xz
fi
## Build binutils
mkdir -p binutils-build
cd binutils-build
../binutils-$BINUTILS_VERSION/configure \
--prefix="$ROOT_DIR/$PACKAGE_NAME" \
$BUILD_OPTIONS \
--target=$TARGET_NAME \
--with-float=soft \
--disable-docs \
--disable-nls \
--disable-werror \
|| exit 2
make -j $NUM_JOBS \
|| exit 2
make install-strip \
|| exit 2
cd ..
rm -rf binutils-build
## Build GCC
mkdir -p gcc-build
cd gcc-build
../gcc-$GCC_VERSION/configure \
--prefix="$ROOT_DIR/$PACKAGE_NAME" \
$BUILD_OPTIONS \
--target=$TARGET_NAME \
--with-float=soft \
--disable-docs \
--disable-nls \
--disable-werror \
--disable-libada \
--disable-libssp \
--disable-libquadmath \
--disable-threads \
--disable-libgomp \
--disable-libstdcxx-pch \
--disable-hosted-libstdcxx \
--enable-languages=c,c++ \
--without-isl \
--without-headers \
--with-gnu-as \
--with-gnu-ld \
|| exit 3
make -j $NUM_JOBS \
|| exit 3
make install-strip \
|| exit 3
cd ..
rm -rf gcc-build
## Package toolchain
#cd $PACKAGE_NAME
#zip -9 -r ../$PACKAGE_NAME-$GCC_VERSION.zip . \
# || exit 4
#cd ..
#rm -rf $PACKAGE_NAME
================================================
FILE: .github/workflows/build.yml
================================================
# The GCC toolchain is stored in the GitHub Actions cache after being built. To
# minimize build times, the toolchain build step is skipped if there is a cached
# copy of the toolchain that has not expired.
name: Build examples
on: [ push, pull_request ]
jobs:
build:
name: Run build
runs-on: ubuntu-latest
steps:
- name: Initialize toolchain cache
id: cache
uses: actions/cache@v3
with:
key: toolchain
path: gcc-mipsel-none-elf
- name: Fetch repo contents
uses: actions/checkout@v4
with:
path: ps1-bare-metal
- name: Install prerequisites
run: |
sudo apt-get update -y
sudo apt-get install -y --no-install-recommends ninja-build
- name: Set up Python virtual environment
run: |
python3 -m venv ps1-bare-metal/env
source ps1-bare-metal/env/bin/activate
pip3 install -r ps1-bare-metal/tools/requirements.txt
- name: Build GCC toolchain
if: ${{ steps.cache.outputs.cache-hit != 'true' }}
run: |
ps1-bare-metal/.github/scripts/buildToolchain.sh gcc-mipsel-none-elf mipsel-none-elf
- name: Build examples
run: |
cd ps1-bare-metal
cmake --preset debug -DTOOLCHAIN_PATH=${{ github.workspace }}/gcc-mipsel-none-elf/bin
cmake --build build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build
path: ps1-bare-metal/build
================================================
FILE: .gitignore
================================================
# Do not include any hidden metadata saved by apps and the OS.
desktop.ini
.DS_Store
.vscode/
# Do not include any built or cached files.
build/
env/
.cache/
__pycache__/
*.pyc
*.pyo
# Do not include user-specific workspace and configuration files.
*.code-workspace
CMakeUserPresets.json
================================================
FILE: .markdownlint.json
================================================
{
"line-length": {
"tables": false
},
"emphasis-style": false,
"no-inline-html": false
}
================================================
FILE: CMakeLists.txt
================================================
# ps1-bare-metal - (C) 2023-2025 spicyjpeg
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
cmake_minimum_required(VERSION 3.25)
# Set the path to the toolchain file, which will configure CMake to use the MIPS
# toolchain rather than its default compiler and proceed in turn to execute
# setup.cmake.
set(CMAKE_TOOLCHAIN_FILE "${CMAKE_CURRENT_LIST_DIR}/cmake/toolchain.cmake")
# Tell CMake about the project. The VERSION, DESCRIPTION and HOMEPAGE_URL fields
# are optional, but the project name and LANGUAGES field should be present.
project(
ps1-bare-metal
LANGUAGES C CXX ASM
VERSION 1.0.0
DESCRIPTION "PlayStation 1 bare-metal C examples"
HOMEPAGE_URL "https://github.com/spicyjpeg/ps1-bare-metal"
)
# Set up compiler flags and initialize the Python environment used to run the
# scripts in the tools directory.
include(cmake/setup.cmake)
include(cmake/tools.cmake)
#if("${PSXAVENC_PATH}" STREQUAL "PSXAVENC_PATH-NOTFOUND")
#set(skipAudioExamples ON)
#message(WARNING "Unable to find psxavenc. All examples that require it for \
#audio encoding will be skipped.")
#endif()
# Build a "common" library containing code shared across all examples and link
# it by default into every executable.
add_library(
common OBJECT
src/libc/clz.s
src/libc/crt0.c
src/libc/cxxsupport.cpp
src/libc/malloc.c
src/libc/misc.c
src/libc/setjmp.s
src/libc/string.c
src/libc/string.s
src/ps1/cache.s
src/vendor/printf.c
)
target_include_directories(
common PUBLIC
src
src/libc
)
link_libraries(common)
# Build the examples and convert any required assets.
addPS1Executable(example00_helloWorld src/00_helloWorld/main.c)
addPS1Executable(example01_basicGraphics src/01_basicGraphics/main.c)
addPS1Executable(example02_doubleBuffer src/02_doubleBuffer/main.c)
addPS1Executable(example03_dmaChain src/03_dmaChain/main.c)
addPS1Executable(example04_textures src/04_textures/main.c)
convertImage(
src/04_textures/texture.png 16
example04/textureData.dat
)
addBinaryFile(example04_textures textureData "${PROJECT_BINARY_DIR}/example04/textureData.dat")
addPS1Executable(example05_palettes src/05_palettes/main.c)
convertImage(
src/05_palettes/texture.png 4
example05/textureData.dat
example05/paletteData.dat
)
addBinaryFile(example05_palettes textureData "${PROJECT_BINARY_DIR}/example05/textureData.dat")
addBinaryFile(example05_palettes paletteData "${PROJECT_BINARY_DIR}/example05/paletteData.dat")
addPS1Executable(
example06_fonts
src/06_fonts/gpu.c
src/06_fonts/main.c
)
convertImage(
src/06_fonts/font.png 4
example06/fontTexture.dat
example06/fontPalette.dat
)
addBinaryFile(example06_fonts fontTexture "${PROJECT_BINARY_DIR}/example06/fontTexture.dat")
addBinaryFile(example06_fonts fontPalette "${PROJECT_BINARY_DIR}/example06/fontPalette.dat")
addPS1Executable(
example07_orderingTable
src/07_orderingTable/gpu.c
src/07_orderingTable/main.c
)
addPS1Executable(
example08_spinningCube
src/08_spinningCube/gpu.c
src/08_spinningCube/main.c
src/08_spinningCube/trig.c
)
addPS1Executable(
example09_controllers
src/09_controllers/font.c
src/09_controllers/gpu.c
src/09_controllers/main.c
)
convertImage(
src/09_controllers/font.png 4
example09/fontTexture.dat
example09/fontPalette.dat
)
addBinaryFile(example09_controllers fontTexture "${PROJECT_BINARY_DIR}/example09/fontTexture.dat")
addBinaryFile(example09_controllers fontPalette "${PROJECT_BINARY_DIR}/example09/fontPalette.dat")
================================================
FILE: CMakePresets.json
================================================
{
"version": 6,
"cmakeMinimumRequired": {
"major": 3,
"minor": 25,
"patch": 0
},
"configurePresets": [
{
"name": "debug",
"displayName": "Debug build",
"description": "Build the project with no optimization and assertions enabled.",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "release",
"displayName": "Release build",
"description": "Build the project with performance optimization and assertions stripped.",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
},
{
"name": "min-size-release",
"displayName": "Minimum size release build",
"description": "Build the project with size optimization and assertions stripped.",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "MinSizeRel"
}
}
]
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2023 spicyjpeg
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# PlayStation 1 bare-metal C examples
This repository contains a series of homebrew tutorials and well-commented
examples for the original Sony PlayStation, built using no external SDKs or
tools other than an up-to-date, unmodified GCC toolchain targeting the MIPS
architecture, CMake as the build system and some Python scripts.
The following examples are currently available:
| # | Screenshot | Description |
| --: | :---------------------------------------------------------------------------- | :-------------------------------------------------------------------------------- |
| 0 | | [Printing "hello world" over the serial port](src/00_helloWorld/main.c) |
| 1 | <img alt="Example 1" src="src/01_basicGraphics/screenshot.png" width="100" /> | [Initializing the GPU and drawing basic graphics](src/01_basicGraphics/main.c) |
| 2 | <img alt="Example 2" src="src/02_doubleBuffer/screenshot.png" width="100" /> | [Adding double buffering and animated graphics](src/02_doubleBuffer/main.c) |
| 3 | <img alt="Example 3" src="src/03_dmaChain/screenshot.png" width="100" /> | [Improving GPU drawing efficiency using DMA chains](src/03_dmaChain/main.c) |
| 4 | <img alt="Example 4" src="src/04_textures/screenshot.png" width="100" /> | [Uploading a texture to VRAM and using it](src/04_textures/main.c) |
| 5 | <img alt="Example 5" src="src/05_palettes/screenshot.png" width="100" /> | [Using indexed color textures and color palettes](src/05_palettes/main.c) |
| 6 | <img alt="Example 6" src="src/06_fonts/screenshot.png" width="100" /> | [Implementing spritesheets and simple font rendering](src/06_fonts/main.c) |
| 7 | <img alt="Example 7" src="src/07_orderingTable/screenshot.png" width="100" /> | [Using ordering tables to control GPU drawing order](src/07_orderingTable/main.c) |
| 8 | <img alt="Example 8" src="src/08_spinningCube/screenshot.png" width="100" /> | [Drawing a 3D spinning cube using the GTE](src/08_spinningCube/main.c) |
| 9 | <img alt="Example 9" src="src/09_controllers/screenshot.png" width="100" /> | [Getting input from connected controllers](src/09_controllers/main.c) |
New examples showing how to make use of more hardware features will be added
over time.
## Building the examples
### Installing dependencies
The following dependencies are required in order to compile the examples:
- CMake 3.25 or later;
- Python 3.10 or later;
- [Ninja](https://ninja-build.org/);
- a recent GCC toolchain configured for the `mipsel-none-elf` target triplet
(toolchains targeting `mipsel-linux-gnu` will generally work as well, but are
not recommended as the ones available in most distros' package managers tend
to be outdated or configured improperly).
The toolchain can be installed on Windows through
[the `mips` script from the pcsx-redux project](https://github.com/grumpycoders/pcsx-redux/tree/main/src/mips/psyqo/GETTING_STARTED.md#windows),
on macOS using
[Homebrew](https://github.com/grumpycoders/pcsx-redux/tree/main/src/mips/psyqo/GETTING_STARTED.md#macos)
or on Linux by
[spawning it from source](https://github.com/grumpycoders/pcsx-redux/blob/main/tools/linux-mips/spawn-compiler.sh),
and should be added to your `PATH` environment variable in order to let CMake
find it. If you have any of the open-source PS1 SDKs installed there is a good
chance you already have a suitable toolchain set up (try running
`mipsel-none-elf-gcc` and `mipsel-linux-gnu-gcc` in a terminal). The other
dependencies can be obtained through a package manager.
The Python scripts require a few additional dependencies, which can be installed
in a virtual environment by running the following commands from the root
directory of the repository:
```bash
# Windows (using PowerShell)
py -m venv env
env\Scripts\Activate.ps1
py -m pip install -r tools\requirements.txt
# Windows (using Cygwin/MSys2), Linux or macOS
python3 -m venv env
source env/bin/activate
pip3 install -r tools/requirements.txt
```
### Building with an IDE
Many IDEs and text editors feature out-of-the-box support for CMake, so you
should be able to import the repo into your IDE of choice and immediately get a
working "build" button once the toolchain is set up. If you are using VS Code,
installing the
[CMake Tools](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cmake-tools)
and [clangd](https://clangd.llvm.org) extensions for build integration as well
as context-sensitive suggestions is highly recommended.
If the toolchain is not listed in your `PATH` environment variable, you will
have to set the `TOOLCHAIN_PATH` CMake variable to the full path to your
toolchain's `bin` subdirectory (e.g. `/opt/mipsel-linux-gnu/bin`) using your
IDE's CMake cache editor. See your IDE's documentation for information on
accessing the cache editor; in VS Code with the CMake Tools extension, the
editor can be opened by selecting "Edit CMake Cache (UI)" from the command
palette (Ctrl+Shift+P).
### Building from the command line
If you cannot use an IDE or prefer working from the command line, simply run
these two commands from the repository's root:
```bash
cmake --preset debug
cmake --build build
```
If you are unfamiliar with CMake, the first command is what's known as the
*configure command* and prepares the build directory for the second command,
which actually runs the compiler and generates the executables. Once the build
directory is prepared you'll no longer have to run the configure command unless
you edit the CMake scripts to e.g. add new examples or source files.
You may replace `debug` with `release` to enable release mode, which will turn
on additional compiler optimizations, remove assertions and produce smaller
binaries. Replacing it with `min-size-release` will further optimize the
executables for size at the expense of performance.
If the toolchain is not listed in your `PATH` environment variable, you will
have to pass the path to its `bin` subdirectory to the configure command via the
`-DTOOLCHAIN_PATH` option, like this:
```bash
cmake --preset debug -DTOOLCHAIN_PATH=/opt/mipsel-linux-gnu/bin
```
### Floating point support
The PlayStation does not have a floating point unit. While GCC can still provide
support for floats through emulation, some prebuilt versions of the
`mipsel-none-elf` and `mipsel-linux-gnu` toolchains ship with floating point
emulation partially or fully disabled at build time. For this reason, the build
scripts do not explicitly enable it.
If you have a toolchain known to have full support for software floating point
(e.g. one you built yourself with the appropriate options) but that does not
have it enabled by default, you may force it by adding the following line to
`CMakeLists.txt`:
```cmake
target_compile_options(flags INTERFACE -msoft-float)
```
Keep in mind that software floats are particularly slow, code-heavy and shall be
avoided at all costs. The included printf library has been modified to disable
the `%f` specifier, regardless of whether the toolchain supports software
floats, in order to reduce code size.
## Running the examples
### Using an emulator
The build scripts will compile each example into a `.psexe` file. This is the
PS1 BIOS's native executable format and is also supported by pretty much every
PS1 emulator, making it straightforward to run the examples through emulation.
The following emulators are recommended for development work:
- [DuckStation](https://github.com/stenzek/duckstation);
- [PCSX-Redux](https://github.com/grumpycoders/pcsx-redux) (not to be confused
with PCSX-R), somewhat less accurate but comes with extensive debugging tools
and scripting support.
Note that both DuckStation and Redux default to using a dynamic recompiler
(dynarec) in order to boost performance at the cost of accuracy. The dynarec is
incompatible with either emulator's debugger and can be prone to timing issues;
it's thus recommended to disable it and switch to interpreted CPU mode instead.
Using other emulators is strongly discouraged as more or less all of them are
outdated and known to be inaccurate. In particular, **the emulators listed**
**below are broken in many ways** and will struggle to run anything not made
using Sony's libraries or not following their practices.
- ePSXe, pSX, XEBRA;
- PCSX forks other than Redux (PCSX-R, Rearmed and so on);
- Beetle PSX, SwanStation and other RetroArch-specific hacked up forks of
existing emulators;
- MAME and anything based on it - there are ongoing efforts to significantly
improve its accuracy but they still have to be upstreamed;
- Sony's official emulators (the PS3 and PSP's built-in backwards compatibility
feature, POPS/POPStarter on the PS2).
The following emulators have generally acceptable accuracy but are not
recommended due to their poor user experience:
- Mednafen (hard to set up, not as well documented as the other options);
- no$psx (Windows only, rarely updated, too many bugs and idiosyncracies in the
UI).
### Using real hardware
At some point you will likely want to run your code on an actual PlayStation.
The two main ways to do so are:
- using a loader program such as
[Unirom](https://github.com/JonathanDotCel/unirom8_bootdisc_and_firmware_for_ps1),
which will allow you to temporarily load the executable into RAM through the
serial port on the back of the console;
- by authoring a disc image and either burning it to a CD-R or loading it onto
an optical drive emulator.
CD-ROM image creation is not currently covered here as it involves using
specialized tools and, depending on the region and revision of your PS1, a
license file. If you have a non-Japanese region unit with a modchip or softmod
(e.g. Unirom installed to a memory card), you may simply rename your executable
to `PSX.EXE`, burn it to a CD-R and the console *should* run it. Unirom also
comes with a file browser that will let you launch any executable on the disc.
Note that a PS2 is *not* a PS1, not even in its "native" (non-POPS) backwards
compatibility mode. It's a chimera of real hardware, emulated hardware,
semi-emulated hardware that
[differs across revisions](https://israpps.github.io/PPC-Monitor/docs/Architecture%20Overview.html)
and game-specific hacks in
[multiple](https://psi-rockin.github.io/ps2tek/#biospscompatibility)
[places](https://israpps.github.io/PPC-Monitor/docs/XPARAM.html). As such, it's
best left alongside the inaccurate emulators and not used for PS1 homebrew
development.
## Modifying the code
If you want to write your own examples or projects, here's a quick overview of
the non-example subfolders in the `src` directory:
- `src/libc` contains a minimal implementation of the C standard library, which
should be enough for most purposes. Some functions have been replaced with
optimized assembly implementations.
- `src/ps1` contains a basic support library for the hardware, consisting mostly
of definitions for hardware registers and GPU commands.
- `src/vendor` is for third-party libraries (currently only the printf library).
If you create a new folder and want its contents to be built, remember to add it
to `CMakeLists.txt` (you may use the existing entries as a reference) and rerun
both the CMake configure and build commands afterwards.
## Background
I have been occasionally asked if I could provide an example of PS1 homebrew
programming that is completely self-contained, permissively licensed and does
not depend on an external SDK. While there are a number of PS1 SDK options
around (including some I have contributed to), their workflows may not suit
everyone and some of the most popular options are additionally encumbered with
legal issues that make them a non-starter for commercial homebrew games, and/or
limitations that are hard to work around. As I have been moving away from using
such libraries myself, I set out to take what I am currently building for my
projects, clean it up and turn it into a tutorial series for other people to
follow.
I want this repo to be an introduction to bare-metal platforms and the PS1 for
anybody who already has some experience with C/C++ but not necessarily with the
process of linking and compiling, the internals of a standard library, the way
threads and IRQs work at the kernel level and so on. I strongly believe that
demystifying the inner workings of a platform can go a long way when it comes to
helping people understand it. Most 8-bit and 16-bit consoles have received a lot
of attention and excellent bare-metal tutorials have been written for them, so I
don't get why people shall just give up and use ancient proprietary SDKs from
the 1990s when it comes to the PS1.
## License
Everything in this repository, including the vendored copy of
[Marco Paland's printf library](https://github.com/mpaland/printf), is licensed
under the MIT license (or the functionally equivalent ISC license). The only
"hard" requirements are attribution and preserving the license notice; you may
otherwise freely use any of the code for both non-commercial and commercial
purposes (such as a paid homebrew game or a book or course).
## See also
- If you are just getting started with PS1 development, Rodrigo Copetti's
[PlayStation Architecture](https://copetti.org/writings/consoles/playstation)
is a great overview of the console's hardware and capabilities.
- The [PlayStation specifications (psx-spx)](https://psx-spx.consoledev.net/)
page, adapted and expanded from no$psx's documentation, is the main hardware
reference for bare-metal PS1 programming and emulation.
- [573in1](https://github.com/spicyjpeg/573in1) is a real world example of a
moderately complex project built on top of the scripts and support library
provided in this repository.
- If you need help or wish to discuss PS1 homebrew development more in general,
you may want to check out the
[PSX.Dev Discord server](https://discord.gg/QByKPpH).
================================================
FILE: cmake/executable.ld
================================================
/*
* ps1-bare-metal - (C) 2023-2025 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
ENTRY(_start)
MEMORY {
/*
* Only 2 MB of main RAM are available on a regular console, but some
* development kits are fitted with 8 MB and most emulators have an option
* to extend RAM to 8 MB as well. You may change the length below from
* 0x1f0000 to 0x7f0000 allow the linker to use the additional memory. Note
* that the first 64 KB at 0x80000000-0x8000ffff are always reserved for use
* by the kernel.
*/
APP_RAM (rwx) : ORIGIN = 0x80010000, LENGTH = 0x1f0000
}
SECTIONS {
/* Code sections */
.text : ALIGN(4) {
_textStart = .;
*(.text .text.* .gnu.linkonce.t.*)
*(.plt .MIPS.stubs)
} > APP_RAM
.rodata : {
*(.rodata .rodata.* .gnu.linkonce.r.*)
_textEnd = .;
} > APP_RAM
/* Global constructor/destructor arrays */
.preinit_array : ALIGN(4) {
_preinitArrayStart = .;
KEEP(*(.preinit_array))
_preinitArrayEnd = .;
} > APP_RAM
.init_array : ALIGN(4) {
_initArrayStart = .;
KEEP(*(SORT(.init_array.*) SORT(.ctors.*)))
KEEP(*(.init_array .ctors))
_initArrayEnd = .;
} > APP_RAM
.fini_array : ALIGN(4) {
_finiArrayStart = .;
KEEP(*(.fini_array .dtors))
KEEP(*(SORT(.fini_array.*) SORT(.dtors.*)))
_finiArrayEnd = .;
} > APP_RAM
/* Data sections */
.data : {
_dataStart = .;
*(.data .data.* .gnu.linkonce.d.*)
} > APP_RAM
/*
* Set _gp (copied to $gp) to point to the beginning of .sdata plus 0x8000,
* so anything within .sdata and .sbss can be accessed using the $gp
* register as base plus a signed 16-bit immediate.
*/
.sdata : {
_gp = ALIGN(16) + 0x7ff0;
*(.sdata .sdata.* .gnu.linkonce.s.*)
_dataEnd = .;
} > APP_RAM
/*
* Ensure the entire BSS region is aligned at both ends in order to allow
* for fast clearing.
*/
.sbss (NOLOAD) : ALIGN(4) {
_bssStart = .;
*(.sbss .sbss.* .gnu.linkonce.sb.*)
*(.scommon)
} > APP_RAM
.bss (NOLOAD) : {
*(.bss .bss.* .gnu.linkonce.b.*)
*(COMMON)
. = ALIGN(4);
_bssEnd = .;
} > APP_RAM
/* Dummy sections */
.dummy (NOLOAD) : {
KEEP(*(.dummy))
} > APP_RAM
/DISCARD/ : {
*(.note.* .gnu_debuglink .gnu.lto_*)
*(.MIPS.abiflags)
}
}
================================================
FILE: cmake/setup.cmake
================================================
# ps1-bare-metal - (C) 2023-2024 spicyjpeg
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
cmake_minimum_required(VERSION 3.25)
# Override the default file extensions for executables and libraries. This is
# not strictly required, but it makes CMake's behavior more consistent.
set(CMAKE_EXECUTABLE_SUFFIX .elf)
set(CMAKE_STATIC_LIBRARY_PREFIX lib)
set(CMAKE_STATIC_LIBRARY_SUFFIX .a)
# Add libgcc.a (-lgcc) to the set of libraries linked to all executables by
# default. This library ships with GCC and must be linked to anything compiled
# with it.
link_libraries(-lgcc)
# Create a dummy "flags" library that is not made up of any files, but adds the
# appropriate compiler and linker flags for PS1 executables to anything linked
# to it. The library is then added to the default set of libraries.
add_library (flags INTERFACE)
link_libraries(flags)
target_compile_features(
flags INTERFACE
c_std_17
cxx_std_20
)
target_compile_options(
flags INTERFACE
-g
-Wall
-Wa,--strip-local-absolute
-ffreestanding
-fno-builtin
-fno-pic
-nostdlib
-fdata-sections
-ffunction-sections
-fsigned-char
-fno-strict-overflow
-march=r3000
-mabi=32
-mfp32
#-msoft-float
-mno-mt
-mno-llsc
-mno-abicalls
-mgpopt
-mno-extern-sdata
-G8
$<$<COMPILE_LANGUAGE:CXX>:
# These options will only be added when compiling C++ source files.
-fno-exceptions
-fno-rtti
-fno-unwind-tables
-fno-threadsafe-statics
-fno-use-cxa-atexit
>
$<IF:$<CONFIG:Debug>,
# These options will only be added if CMAKE_BUILD_TYPE is set to Debug.
-Og
-mdivide-breaks
,
# These options will be added if CMAKE_BUILD_TYPE is not set to Debug.
#-O3
#-flto
-mno-check-zero-division
>
)
target_link_options(
flags INTERFACE
-static
-nostdlib
-Wl,-gc-sections
-G8
"-T${CMAKE_CURRENT_LIST_DIR}/executable.ld"
)
# Define a helper function to embed binary data into executables and libraries.
function(addBinaryFile target name path)
set(asmFile "${PROJECT_BINARY_DIR}/includes/${target}_${name}.s")
cmake_path(ABSOLUTE_PATH path OUTPUT_VARIABLE fullPath)
# Generate an assembly listing that uses the .incbin directive to embed the
# file and add it to the executable's list of source files. This may look
# hacky, but it works and lets us easily customize the symbol name (i.e. the
# name of the "array" that will contain the file's data).
file(
CONFIGURE
OUTPUT "${asmFile}"
CONTENT [[
.section .rodata.${name}, "a"
.balign 8
.global ${name}
.type ${name}, @object
.size ${name}, (${name}_end - ${name})
${name}:
.incbin "${fullPath}"
${name}_end:
]]
ESCAPE_QUOTES
NEWLINE_STYLE LF
)
target_sources(${target} PRIVATE "${asmFile}")
set_source_files_properties(
"${asmFile}" PROPERTIES OBJECT_DEPENDS "${fullPath}"
)
endfunction()
function(addBinaryFileWithSize target name sizeName path)
set(asmFile "${PROJECT_BINARY_DIR}/includes/${target}_${name}.s")
cmake_path(ABSOLUTE_PATH path OUTPUT_VARIABLE fullPath)
file(
CONFIGURE
OUTPUT "${asmFile}"
CONTENT [[
.section .rodata.${name}, "a"
.balign 8
.global ${name}
.type ${name}, @object
.size ${name}, (${name}_end - ${name})
${name}:
.incbin "${fullPath}"
${name}_end:
.section .rodata.${sizeName}, "a"
.balign 4
.global ${sizeName}
.type ${sizeName}, @object
.size ${sizeName}, 4
${sizeName}:
.int (${name}_end - ${name})
]]
ESCAPE_QUOTES
NEWLINE_STYLE LF
)
target_sources(${target} PRIVATE "${asmFile}")
set_source_files_properties(
"${asmFile}" PROPERTIES OBJECT_DEPENDS "${fullPath}"
)
endfunction()
================================================
FILE: cmake/toolchain.cmake
================================================
# ps1-bare-metal - (C) 2023-2024 spicyjpeg
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
cmake_minimum_required(VERSION 3.25)
# Create a user-editable variable to allow for a custom toolchain path to be
# specified by passing -DTOOLCHAIN_PATH=... to CMake.
set(
TOOLCHAIN_PATH ""
CACHE PATH "Directory containing GCC toolchain (if not listed in PATH)"
)
# Prevent CMake from using any host compiler by manually overriding the platform
# and setting it to "generic" (i.e. no defaults).
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR mipsel)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
# Tell CMake not to run the linker when testing the toolchain and to pass our
# custom variables through to autogenerated "compiler test" projects. This will
# prevent the compiler detection process from erroring out.
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
set(CMAKE_TRY_COMPILE_PLATFORM_VARIABLES TOOLCHAIN_PATH)
# Always generate compile_commands.json when building. This allows some IDEs and
# tools (such as clangd) to automatically configure include directories and
# other options.
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
## Toolchain path setup
# Attempt to locate the GCC command in the provided path (if any) as well as in
# the system's standard paths for programs such as the ones listed in the PATH
# environment variable. Try to use a mipsel-none-elf toolchain over a
# mipsel-linux-gnu one if available.
find_program(
gccBinaryPath
mipsel-none-elf-gcc
mipsel-unknown-elf-gcc
mipsel-linux-gnu-gcc
HINTS
"${TOOLCHAIN_PATH}"
"${TOOLCHAIN_PATH}/bin"
"${TOOLCHAIN_PATH}/../bin"
NO_CACHE
)
if("${gccBinaryPath}" STREQUAL "gccBinaryPath-NOTFOUND")
message(FATAL_ERROR "Unable to find the GCC toolchain. Ensure your PATH \
environment variable includes the full path to the toolchain's /bin subfolder, \
or pass -DTOOLCHAIN_PATH=... to CMake to specify its location manually.")
endif()
cmake_path(GET gccBinaryPath PARENT_PATH toolchainPath)
# If a valid path was not provided but GCC was found, overwrite the variable to
# avoid searching again the next time the project is configured.
if(NOT IS_DIRECTORY "${TOOLCHAIN_PATH}")
set(
TOOLCHAIN_PATH "${toolchainPath}"
CACHE PATH "Directory containing GCC toolchain (if not listed in PATH)"
FORCE
)
endif()
# Set the paths to all tools required by CMake. The appropriate extension for
# executables (.exe on Windows, none on Unix) is extracted from the path to GCC
# using a regular expression, as CMake does not otherwise expose it when
# cross-compiling.
string(REGEX MATCH "^(.+-)gcc(.*)$" dummy "${gccBinaryPath}")
set(CMAKE_ASM_COMPILER "${CMAKE_MATCH_1}gcc${CMAKE_MATCH_2}")
set(CMAKE_C_COMPILER "${CMAKE_MATCH_1}gcc${CMAKE_MATCH_2}")
set(CMAKE_CXX_COMPILER "${CMAKE_MATCH_1}g++${CMAKE_MATCH_2}")
set(CMAKE_AR "${CMAKE_MATCH_1}ar${CMAKE_MATCH_2}")
set(CMAKE_LINKER "${CMAKE_MATCH_1}gcc${CMAKE_MATCH_2}")
set(CMAKE_RANLIB "${CMAKE_MATCH_1}ranlib${CMAKE_MATCH_2}")
set(CMAKE_OBJCOPY "${CMAKE_MATCH_1}objcopy${CMAKE_MATCH_2}")
set(CMAKE_OBJDUMP "${CMAKE_MATCH_1}objdump${CMAKE_MATCH_2}")
set(CMAKE_NM "${CMAKE_MATCH_1}nm${CMAKE_MATCH_2}")
set(CMAKE_SIZE "${CMAKE_MATCH_1}size${CMAKE_MATCH_2}")
set(CMAKE_STRIP "${CMAKE_MATCH_1}strip${CMAKE_MATCH_2}")
set(CMAKE_READELF "${CMAKE_MATCH_1}readelf${CMAKE_MATCH_2}")
================================================
FILE: cmake/tools.cmake
================================================
# ps1-bare-metal - (C) 2023-2025 spicyjpeg
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
cmake_minimum_required(VERSION 3.25)
# Create a user-editable variable to allow for the path to the Python virtual
# environment to be customized by passing -DVENV_PATH=... to CMake.
set(
VENV_PATH "${PROJECT_SOURCE_DIR}/env"
CACHE PATH "Directory containing Python virtual environment"
)
# If no virtual environment was activated prior to building, attempt to set up
# the one specified by the variable.
if(NOT IS_DIRECTORY "$ENV{VIRTUAL_ENV}")
if(IS_DIRECTORY "${VENV_PATH}")
set(ENV{VIRTUAL_ENV} "${VENV_PATH}")
else()
message(FATAL_ERROR "Unable to find the Python virtual environment. \
Refer to the README to set one up in ${VENV_PATH}, or pass -DVENV_PATH=... to \
CMake to specify its location manually.")
endif()
endif()
# Activate the environment by letting CMake search for its Python interpreter.
set(Python3_FIND_VIRTUALENV ONLY)
find_package(Python3 3.10 REQUIRED COMPONENTS Interpreter)
# Define some helper functions that rely on the Python scripts in the tools
# folder.
function(addPS1Executable name)
add_executable(${name} ${ARGN})
# As the GCC linker outputs executables in ELF format, a script must be run
# on each compiled binary to convert it to the .psexe format expected by the
# PS1. By default all custom commands run from the build directory, so paths
# to files in the source tree must be relative to ${PROJECT_SOURCE_DIR} or
# ${CMAKE_CURRENT_FUNCTION_LIST_DIR}.
add_custom_command(
TARGET ${name} POST_BUILD
BYPRODUCTS ${name}.psexe
COMMAND
"${Python3_EXECUTABLE}"
"${CMAKE_CURRENT_FUNCTION_LIST_DIR}/../tools/convertExecutable.py"
"$<TARGET_FILE:${name}>"
${name}.psexe
VERBATIM
)
endfunction()
function(addPS1ExecutableAdv name loadAddress stackTop region)
add_executable (${name} ${ARGN})
target_link_options(${name} PRIVATE "-Ttext=${loadAddress}")
add_custom_command(
TARGET ${name} POST_BUILD
BYPRODUCTS "${name}.psexe"
COMMAND
"${Python3_EXECUTABLE}"
"${CMAKE_CURRENT_FUNCTION_LIST_DIR}/../tools/convertExecutable.py"
-r "${region}"
-s "${stackTop}"
"$<TARGET_FILE:${name}>"
${name}.psexe
VERBATIM
)
endfunction()
function(convertImage input bpp)
add_custom_command(
OUTPUT ${ARGN}
DEPENDS "${PROJECT_SOURCE_DIR}/${input}"
COMMAND
"${Python3_EXECUTABLE}"
"${CMAKE_CURRENT_FUNCTION_LIST_DIR}/../tools/convertImage.py"
-b ${bpp}
"${PROJECT_SOURCE_DIR}/${input}"
${ARGN}
VERBATIM
)
endfunction()
# Let CMake locate psxavenc automatically (or rely on the user overriding it by
# passing -DPSXAVENC_PATH=...) and define a helper function to encode audio
# samples if available.
find_program(
PSXAVENC_PATH psxavenc
DOC "Path to psxavenc executable (if not present in PATH)"
)
function(convertAudioSample input sampleRate output)
if("${PSXAVENC_PATH}" STREQUAL "PSXAVENC_PATH-NOTFOUND")
message(FATAL_ERROR "Unable to find psxavenc. Ensure your PATH \
environment variable includes the full path to the directory containing it, or \
pass -DPSXAVENC_PATH=... to CMake to specify its location manually.")
endif()
add_custom_command(
OUTPUT "${output}"
DEPENDS "${PROJECT_SOURCE_DIR}/${input}"
COMMAND
"${PSXAVENC_PATH}"
-t spu
-f ${sampleRate}
"${PROJECT_SOURCE_DIR}/${input}"
"${output}"
VERBATIM
)
endfunction()
================================================
FILE: src/00_helloWorld/main.c
================================================
/*
* ps1-bare-metal - (C) 2023-2025 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
/*
* Let's start with the absolute basics, the obligatory hello world program. We
* are going to just print "Hello world!" in an infinite loop; since we don't
* have the luxury of a terminal or even anything resembling a "text mode" on
* the GPU, we'll use the PS1's serial port instead.
*
* The serial port can be found on the back of the console on all models except
* the PSone, and can be connected to a PC with an appropriately modified link
* cable. Internally it is connected to the secondary serial interface, known as
* SIO1 (as opposed to SIO0 which is wired to the controller and memory card
* ports). SIO1 is controlled through I/O registers, which we're going to
* manipulate to get it to output our message.
*/
#include "ps1/registers.h"
static void printCharacter(char ch) {
// Wait until the serial interface is ready to send a new byte, then write
// it to the data register.
// NOTE: the serial interface checks for an external signal (CTS) and will
// *not* send any data until it is asserted. To avoid blocking forever if
// CTS is not asserted, we have to check for it manually and abort if
// necessary.
while (
(SIO_STAT(1) & (SIO_STAT_TX_NOT_FULL | SIO_STAT_CTS)) == SIO_STAT_CTS
)
__asm__ volatile("");
if (SIO_STAT(1) & SIO_STAT_CTS)
SIO_DATA(1) = ch;
}
int main(int argc, const char **argv) {
// Reset the serial interface and initialize it to output data at 115200bps,
// 8 data bits, 1 stop bit and no parity.
SIO_CTRL(1) = SIO_CTRL_RESET;
SIO_MODE(1) = 0
| SIO_MODE_BAUD_DIV1
| SIO_MODE_DATA_8
| SIO_MODE_STOP_1;
SIO_BAUD(1) = F_CPU / 115200;
SIO_CTRL(1) = 0
| SIO_CTRL_TX_ENABLE
| SIO_CTRL_RX_ENABLE
| SIO_CTRL_RTS;
// Output "Hello world!" in a loop, one character at a time.
for (;;) {
const char *str = "Hello world!\n";
for (; *str; str++)
printCharacter(*str);
}
// We're not actually going to return. Unless a loader was used to launch
// the program, returning from main() would crash the console as there would
// be nothing to return to.
return 0;
}
================================================
FILE: src/01_basicGraphics/main.c
================================================
/*
* ps1-bare-metal - (C) 2023-2025 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
/*
* In this tutorial we're going to initialize the GPU, set it up and display
* some simple hardware-rendered graphics (namely, a shaded triangle).
*
* While the PS1's GPU may appear complicated and daunting, its principle of
* operation is in actual fact very simple. At a high level it is simply a
* "rasterization machine" capable of drawing triangles, quads, rectangles and
* lines in 2D space (with 3D transformations being entirely the CPU's
* responsibility) and of displaying a rectangular cutout of its 1024x512x16bpp
* framebuffer, which resides in dedicated memory (VRAM). It is controlled using
* only two registers, one (GP0) for drawing commands and the other (GP1) for
* display control and other commands; we're going to see how to configure the
* video output and draw our triangle by writing the right commands to these
* registers.
*
* This tutorial will use the ps1/gpucmd.h header file I wrote, which contains
* enumerations and inline functions for all commands supported by the GPU. If
* you wish to write such a header yourself, you'll want to check out the
* GPU register and command documentation at:
* https://psx-spx.consoledev.net/graphicsprocessingunitgpu
*/
#include <stdio.h>
#include "ps1/gpucmd.h"
#include "ps1/registers.h"
static void setupGPU(GP1VideoMode mode, int width, int height) {
// Set the origin of the displayed framebuffer. These "magic" values,
// derived from the GPU's internal clocks, will center the picture on most
// displays and upscalers.
int x = 0x760;
int y = (mode == GP1_MODE_PAL) ? 0xa3 : 0x88;
// Set the resolution. The GPU provides a number of fixed horizontal (256,
// 320, 368, 512, 640) and vertical (240-256, 480-512) resolutions to pick
// from, which affect how fast pixels are output and thus how "stretched"
// the framebuffer will appear.
GP1HorizontalRes horizontalRes = GP1_HRES_320;
GP1VerticalRes verticalRes = GP1_VRES_256;
// Set the number of displayed rows and columns. These values are in GPU
// clock units rather than pixels, thus they are dependent on the selected
// resolution.
int offsetX = (width * gp1_clockMultiplierH(horizontalRes)) / 2;
int offsetY = (height / gp1_clockDividerV(verticalRes)) / 2;
// Hand all parameters over to the GPU by sending GP1 commands. The last
// command unblanks (turns on) the video output, as resetting the GPU blanks
// it by default.
GPU_GP1 = gp1_resetGPU();
GPU_GP1 = gp1_fbRangeH(x - offsetX, x + offsetX);
GPU_GP1 = gp1_fbRangeV(y - offsetY, y + offsetY);
GPU_GP1 = gp1_fbMode(
horizontalRes,
verticalRes,
mode,
false,
GP1_COLOR_16BPP
);
GPU_GP1 = gp1_dispBlank(false);
}
static void waitForGP0Ready(void) {
// Block until the GPU reports to be ready to accept commands through its
// status register (which has the same address as GP1 but is read-only).
while (!(GPU_GP1 & GP1_STAT_CMD_READY))
__asm__ volatile("");
}
#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 240
int main(int argc, const char **argv) {
// Initialize the serial interface. The initSerialIO() function is defined
// in src/libc/misc.c and does basically the same things we did in the
// previous example. Afterwards, we'll be able to use puts(), printf() and
// a few other standard I/O functions as they are declared in the libc
// directory (with printf() being provided by a third-party library).
initSerialIO(115200);
// Read the GPU's status register to check if it was left in PAL or NTSC
// mode by the BIOS/loader.
if ((GPU_GP1 & GP1_STAT_FB_MODE_BITMASK) == GP1_STAT_FB_MODE_PAL) {
puts("Using PAL mode");
setupGPU(GP1_MODE_PAL, SCREEN_WIDTH, SCREEN_HEIGHT);
} else {
puts("Using NTSC mode");
setupGPU(GP1_MODE_NTSC, SCREEN_WIDTH, SCREEN_HEIGHT);
}
// Wait for the GPU to become ready, then send some GP0 commands to tell it
// which area of the framebuffer we want to draw to and enable dithering.
waitForGP0Ready();
GPU_GP0 = gp0_setPage(0, true, false);
GPU_GP0 = gp0_fbOffset1(0, 0);
GPU_GP0 = gp0_fbOffset2(SCREEN_WIDTH - 1, SCREEN_HEIGHT - 1);
GPU_GP0 = gp0_fbOrigin(0, 0);
// Send a VRAM fill command to quickly fill our area with solid gray. Note
// that the coordinates passed to this specific command are *not* relative
// to the ones we've just sent to the GPU!
waitForGP0Ready();
GPU_GP0 = gp0_rgb(64, 64, 64) | gp0_vramFill();
GPU_GP0 = gp0_xy(0, 0);
GPU_GP0 = gp0_xy(SCREEN_WIDTH, SCREEN_HEIGHT);
// Tell the GPU to draw a Gouraud shaded triangle whose vertices are red,
// green and blue respectively at the center of our drawing area.
waitForGP0Ready();
GPU_GP0 = gp0_rgb(255, 0, 0) | gp0_shadedTriangle(true, false, false);
GPU_GP0 = gp0_xy(SCREEN_WIDTH / 2, 32);
GPU_GP0 = gp0_rgb(0, 255, 0);
GPU_GP0 = gp0_xy(32, SCREEN_HEIGHT - 32);
GPU_GP0 = gp0_rgb(0, 0, 255);
GPU_GP0 = gp0_xy(SCREEN_WIDTH - 32, SCREEN_HEIGHT - 32);
// Send a GP1 command to set the origin of the area we want to display.
GPU_GP1 = gp1_fbOffset(0, 0);
// Continue by doing nothing.
for (;;)
__asm__ volatile("");
return 0;
}
================================================
FILE: src/02_doubleBuffer/main.c
================================================
/*
* ps1-bare-metal - (C) 2023-2025 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
/*
* We saw how to initialize the GPU and get basic graphics on screen in the last
* tutorial. It's now time to add motion to the mix: we're going to draw a
* square which, in true DVD player screensaver fashion, will bounce around on
* the screen.
*
* This may sound simple in theory, but there are a few caveats we'll have to
* look out for. First of all we will need some sort of timer for our animation,
* ideally something synchronized to the display output in order to avoid
* updating the position of our square while the picture is still being sent by
* the GPU to the monitor (and stabilize the frame rate). We'll also have to
* ensure the frame we are sending in the first place is not actively being
* updated by the GPU, otherwise screen tearing will be prominent. Hiding a
* frame while it is being drawn may sound tricky, but there is a very simple
* way to accomplish it: we are going to keep *two* frames in VRAM, and draw one
* while the other is being displayed. Once drawing is done and the other frame
* has been fully sent to the display, we're going to swap the buffers (so that
* the newly rendered frame will be displayed) and start over.
*
* This is an extremely common practice (the device you are looking at right now
* is no doubt using it) known as double buffering, and you can read more about
* it here:
* https://gameprogrammingpatterns.com/double-buffer.html
*/
#include <stdbool.h>
#include <stdio.h>
#include "ps1/gpucmd.h"
#include "ps1/registers.h"
static void setupGPU(GP1VideoMode mode, int width, int height) {
int x = 0x760;
int y = (mode == GP1_MODE_PAL) ? 0xa3 : 0x88;
GP1HorizontalRes horizontalRes = GP1_HRES_320;
GP1VerticalRes verticalRes = GP1_VRES_256;
int offsetX = (width * gp1_clockMultiplierH(horizontalRes)) / 2;
int offsetY = (height / gp1_clockDividerV(verticalRes)) / 2;
GPU_GP1 = gp1_resetGPU();
GPU_GP1 = gp1_fbRangeH(x - offsetX, x + offsetX);
GPU_GP1 = gp1_fbRangeV(y - offsetY, y + offsetY);
GPU_GP1 = gp1_fbMode(
horizontalRes,
verticalRes,
mode,
false,
GP1_COLOR_16BPP
);
GPU_GP1 = gp1_dispBlank(false);
}
static void waitForGP0Ready(void) {
while (!(GPU_GP1 & GP1_STAT_CMD_READY))
__asm__ volatile("");
}
static void waitForVSync(void) {
// The GPU won't tell us directly whenever it is done sending a frame to the
// display, but it will send a signal to another peripheral known as the
// interrupt controller (which will be covered in a future tutorial). We can
// thus wait until the interrupt controller's vertical blank flag gets set,
// then reset (acknowledge) it so that it can be set again by the GPU.
while (!(IRQ_STAT & (1 << IRQ_VSYNC)))
__asm__ volatile("");
IRQ_STAT = ~(1 << IRQ_VSYNC);
}
#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 240
int main(int argc, const char **argv) {
initSerialIO(115200);
if ((GPU_GP1 & GP1_STAT_FB_MODE_BITMASK) == GP1_STAT_FB_MODE_PAL) {
puts("Using PAL mode");
setupGPU(GP1_MODE_PAL, SCREEN_WIDTH, SCREEN_HEIGHT);
} else {
puts("Using NTSC mode");
setupGPU(GP1_MODE_NTSC, SCREEN_WIDTH, SCREEN_HEIGHT);
}
int x = 0, velocityX = 1;
int y = 0, velocityY = 1;
bool usingSecondFrame = false;
for (;;) {
// Determine the VRAM location of the current frame. We're going to
// place the two frames next to each other in VRAM, at (0, 0) and
// (320, 0) respectively.
int frameX = usingSecondFrame ? SCREEN_WIDTH : 0;
int frameY = 0;
usingSecondFrame = !usingSecondFrame;
// Tell the GPU which area of VRAM belongs to the frame we're going to
// use and enable dithering.
waitForGP0Ready();
GPU_GP0 = gp0_setPage(0, true, false);
GPU_GP0 = gp0_fbOffset1(frameX, frameY);
GPU_GP0 = gp0_fbOffset2(
frameX + SCREEN_WIDTH - 1,
frameY + SCREEN_HEIGHT - 1
);
GPU_GP0 = gp0_fbOrigin(frameX, frameY);
// Fill the framebuffer with solid gray.
waitForGP0Ready();
GPU_GP0 = gp0_rgb(64, 64, 64) | gp0_vramFill();
GPU_GP0 = gp0_xy(frameX, frameY);
GPU_GP0 = gp0_xy(SCREEN_WIDTH, SCREEN_HEIGHT);
// Draw the yellow bouncing square using a rectangle command.
waitForGP0Ready();
GPU_GP0 = gp0_rgb(255, 255, 0) | gp0_rectangle(false, false, false);
GPU_GP0 = gp0_xy(x, y);
GPU_GP0 = gp0_xy(32, 32);
// Update the position of the bouncing square.
x += velocityX;
y += velocityY;
if ((x <= 0) || (x >= (SCREEN_WIDTH - 32)))
velocityX = -velocityX;
if ((y <= 0) || (y >= (SCREEN_HEIGHT - 32)))
velocityY = -velocityY;
// Wait for the GPU to finish drawing and displaying the contents of the
// previous frame, then tell it to start sending the newly drawn frame
// to the video output.
waitForGP0Ready();
waitForVSync();
GPU_GP1 = gp1_fbOffset(frameX, frameY);
}
return 0;
}
================================================
FILE: src/03_dmaChain/main.c
================================================
/*
* ps1-bare-metal - (C) 2023-2025 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
/*
* In the previous two examples we saw how to control the GPU and draw graphics
* by writing commands directly to the GP0 and GP1 registers. While this
* approach is simple and easy to understand, it also limits performance: the
* CPU always has to wait for the GPU to go idle before being able to send it a
* new command, otherwise the GPU may miss it; similarly, the GPU can only
* process commands while the CPU is actively feeding it.
*
* To get around these limitations the GPU (along with most of the PS1's other
* peripherals) can instead be given access to RAM and read GP0 commands from it
* automatically, without needing the CPU to feed it. This is accomplished
* through a middleman peripheral known as the direct memory access (DMA)
* controller, and is the key to high-performance graphics on the PS1. We'll see
* how to allocate a buffer in RAM, fill it with commands and then set up the
* GPU's DMA channel to read from it in the background while we're preparing the
* next frame's command buffer.
*/
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include "ps1/gpucmd.h"
#include "ps1/registers.h"
static void setupGPU(GP1VideoMode mode, int width, int height) {
int x = 0x760;
int y = (mode == GP1_MODE_PAL) ? 0xa3 : 0x88;
GP1HorizontalRes horizontalRes = GP1_HRES_320;
GP1VerticalRes verticalRes = GP1_VRES_256;
int offsetX = (width * gp1_clockMultiplierH(horizontalRes)) / 2;
int offsetY = (height / gp1_clockDividerV(verticalRes)) / 2;
GPU_GP1 = gp1_resetGPU();
GPU_GP1 = gp1_fbRangeH(x - offsetX, x + offsetX);
GPU_GP1 = gp1_fbRangeV(y - offsetY, y + offsetY);
GPU_GP1 = gp1_fbMode(
horizontalRes,
verticalRes,
mode,
false,
GP1_COLOR_16BPP
);
GPU_GP1 = gp1_dispBlank(false);
// Enable and reset the GPU's DMA channel, then tell the GPU to fetch GP0
// commands from DMA whenever available.
DMA_DPCR |= DMA_DPCR_CH_ENABLE(DMA_GPU);
DMA_CHCR(DMA_GPU) = 0;
GPU_GP1 = gp1_dmaRequestMode(GP1_DREQ_GP0_WRITE);
}
static void waitForGP0Ready(void) {
while (!(GPU_GP1 & GP1_STAT_CMD_READY))
__asm__ volatile("");
}
static void waitForVSync(void) {
while (!(IRQ_STAT & (1 << IRQ_VSYNC)))
__asm__ volatile("");
IRQ_STAT = ~(1 << IRQ_VSYNC);
}
static void sendGPULinkedList(const void *data) {
// Wait until the GPU's DMA unit has finished sending data and is ready.
while (DMA_CHCR(DMA_GPU) & DMA_CHCR_ENABLE)
__asm__ volatile("");
// Make sure the pointer is aligned to 32 bits (4 bytes). The DMA engine is
// not capable of reading unaligned data.
assert(!((uint32_t) data % 4));
// Give DMA a pointer to the beginning of the data and tell it to send it in
// linked list mode. The DMA unit will start parsing a chain of "packets"
// from RAM, with each packet being made up of a 32-bit header followed by
// zero or more 32-bit commands to be sent to the GP0 register.
DMA_MADR(DMA_GPU) = (uint32_t) data;
DMA_CHCR(DMA_GPU) = 0
| DMA_CHCR_WRITE
| DMA_CHCR_MODE_LIST
| DMA_CHCR_ENABLE;
}
// Define a structure we'll allocate our linked list packets into. We are going
// to use a fixed-size buffer and keep a pointer to the beginning of its free
// area, incrementing it whenever we allocate a new packet.
#define DMA_MAX_CHUNK_SIZE 16
#define GPU_CHAIN_BUFFER_SIZE 1024
typedef struct {
uint32_t data[GPU_CHAIN_BUFFER_SIZE];
uint32_t *nextPacket;
} GPUDMAChain;
static uint32_t *allocateGP0Packet(GPUDMAChain *chain, int numCommands) {
// Ensure no more than 16 command words are sent to the GPU at once, as
// sending larger packets may overrun the GP0 command FIFO and result in
// corrupted data.
assert((numCommands >= 0) && (numCommands <= DMA_MAX_CHUNK_SIZE));
// Grab the current pointer to the next packet then increment it to allocate
// a new packet. We have to allocate an extra word for the packet's header,
// which will contain the number of GP0 commands the packet is made up of as
// well as a pointer to the next packet (or a special "terminator" value to
// tell the DMA unit to stop).
uint32_t *ptr = chain->nextPacket;
chain->nextPacket += numCommands + 1;
// Write the header and set its pointer to point to the next packet that
// will be allocated in the buffer.
*ptr = gp0_tag(numCommands, chain->nextPacket);
// Make sure we haven't yet run out of space for future packets or a linked
// list terminator, then return a pointer to the packet's first GP0 command.
assert(chain->nextPacket < &(chain->data)[GPU_CHAIN_BUFFER_SIZE]);
return &ptr[1];
}
#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 240
int main(int argc, const char **argv) {
initSerialIO(115200);
if ((GPU_GP1 & GP1_STAT_FB_MODE_BITMASK) == GP1_STAT_FB_MODE_PAL) {
puts("Using PAL mode");
setupGPU(GP1_MODE_PAL, SCREEN_WIDTH, SCREEN_HEIGHT);
} else {
puts("Using NTSC mode");
setupGPU(GP1_MODE_NTSC, SCREEN_WIDTH, SCREEN_HEIGHT);
}
int x = 0, velocityX = 1;
int y = 0, velocityY = 1;
// Allocate a double command buffer in order to let the GPU keep fetching
// commands while the CPU is filling up the other buffer. See the previous
// example for more details on double buffering.
GPUDMAChain dmaChains[2];
bool usingSecondFrame = false;
for (;;) {
int bufferX = usingSecondFrame ? SCREEN_WIDTH : 0;
int bufferY = 0;
GPUDMAChain *chain = &dmaChains[usingSecondFrame];
usingSecondFrame = !usingSecondFrame;
uint32_t *ptr;
// Display the frame that was just drawn by the GPU (if any). We are
// going to overwrite its respective DMA chain afterwards, as the GPU no
// longer needs it.
GPU_GP1 = gp1_fbOffset(bufferX, bufferY);
// Reset the pointer to the next packet to the beginning of the buffer.
chain->nextPacket = chain->data;
// Create a new DMA packet for each GP0 command we're sending. Splitting
// up each command like this will make sure the DMA channel won't try to
// send them too quickly and end up overflowing the GPU's internal
// command processor.
ptr = allocateGP0Packet(chain, 4);
ptr[0] = gp0_setPage(0, true, false);
ptr[1] = gp0_fbOffset1(bufferX, bufferY);
ptr[2] = gp0_fbOffset2(
bufferX + SCREEN_WIDTH - 1,
bufferY + SCREEN_HEIGHT - 1
);
ptr[3] = gp0_fbOrigin(bufferX, bufferY);
ptr = allocateGP0Packet(chain, 3);
ptr[0] = gp0_rgb(64, 64, 64) | gp0_vramFill();
ptr[1] = gp0_xy(bufferX, bufferY);
ptr[2] = gp0_xy(SCREEN_WIDTH, SCREEN_HEIGHT);
ptr = allocateGP0Packet(chain, 3);
ptr[0] = gp0_rgb(255, 255, 0) | gp0_rectangle(false, false, false);
ptr[1] = gp0_xy(x, y);
ptr[2] = gp0_xy(32, 32);
// Terminate the linked list and tell the DMA engine to stop reading by
// appending a terminator header to it.
*(chain->nextPacket) = gp0_endTag(0);
x += velocityX;
y += velocityY;
if ((x <= 0) || (x >= (SCREEN_WIDTH - 32)))
velocityX = -velocityX;
if ((y <= 0) || (y >= (SCREEN_HEIGHT - 32)))
velocityY = -velocityY;
// Wait for the previous frame to be displayed, then start sending the
// newly built DMA chain in the background while the next iteration of
// the main loop is going to run.
waitForGP0Ready();
waitForVSync();
sendGPULinkedList(chain->data);
}
return 0;
}
================================================
FILE: src/04_textures/main.c
================================================
/*
* ps1-bare-metal - (C) 2023-2025 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
/*
* The last example showed how to take advantage of the PS1's DMA engine to send
* commands to the GPU efficiently in the background. While that is by far the
* most common use case for DMA, the GPU's DMA channel has another crucial role:
* it allows for fast data transfers from and to VRAM, which are useful for
* uploading image data to be used as a texture when drawing.
*
* This example shows how to upload raw 16bpp RGB image data embedded into the
* executable to an arbitrary location within the 1024x512 VRAM buffer, store
* its coordinates into memory and recall them later in order to draw a textured
* sprite on screen. The process unfortunately involves dealing with a number of
* GPU idiosyncracies, such as its lack of support for textures larger than
* 256x256 or its requirement for all textures to be arranged into a grid of
* 64x256 regions of VRAM known as "texture pages" (a texture may span up to
* four pages horizontally but only one vertically, so it can't e.g. cross the
* Y=256 boundary). With those details out of the way, however, using textures
* boils down to simply performing a DMA transfer and setting the appropriate
* fields in GP0 commands.
*/
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include "ps1/gpucmd.h"
#include "ps1/registers.h"
static void setupGPU(GP1VideoMode mode, int width, int height) {
int x = 0x760;
int y = (mode == GP1_MODE_PAL) ? 0xa3 : 0x88;
GP1HorizontalRes horizontalRes = GP1_HRES_320;
GP1VerticalRes verticalRes = GP1_VRES_256;
int offsetX = (width * gp1_clockMultiplierH(horizontalRes)) / 2;
int offsetY = (height / gp1_clockDividerV(verticalRes)) / 2;
GPU_GP1 = gp1_resetGPU();
GPU_GP1 = gp1_fbRangeH(x - offsetX, x + offsetX);
GPU_GP1 = gp1_fbRangeV(y - offsetY, y + offsetY);
GPU_GP1 = gp1_fbMode(
horizontalRes,
verticalRes,
mode,
false,
GP1_COLOR_16BPP
);
GPU_GP1 = gp1_dispBlank(false);
DMA_DPCR |= DMA_DPCR_CH_ENABLE(DMA_GPU);
DMA_CHCR(DMA_GPU) = 0;
GPU_GP1 = gp1_dmaRequestMode(GP1_DREQ_GP0_WRITE);
}
static void waitForGP0Ready(void) {
while (!(GPU_GP1 & GP1_STAT_CMD_READY))
__asm__ volatile("");
}
static void waitForGPUDMADone(void) {
while (DMA_CHCR(DMA_GPU) & DMA_CHCR_ENABLE)
__asm__ volatile("");
}
static void waitForVSync(void) {
while (!(IRQ_STAT & (1 << IRQ_VSYNC)))
__asm__ volatile("");
IRQ_STAT = ~(1 << IRQ_VSYNC);
}
static void sendGPULinkedList(const void *data) {
waitForGPUDMADone();
assert(!((uint32_t) data % 4));
DMA_MADR(DMA_GPU) = (uint32_t) data;
DMA_CHCR(DMA_GPU) = 0
| DMA_CHCR_WRITE
| DMA_CHCR_MODE_LIST
| DMA_CHCR_ENABLE;
}
#define DMA_MAX_CHUNK_SIZE 16
static void sendVRAMData(
const void *data,
int x,
int y,
int width,
int height
) {
waitForGPUDMADone();
assert(!((uint32_t) data % 4));
// Calculate how many 32-bit words will be sent from the width and height of
// the texture. If more than 16 words have to be sent, configure DMA to
// split the transfer into 16-word chunks in order to make sure the GPU will
// not miss any data.
size_t length = (width * height + 1) / 2;
size_t chunkSize, numChunks;
if (length < DMA_MAX_CHUNK_SIZE) {
chunkSize = length;
numChunks = 1;
} else {
chunkSize = DMA_MAX_CHUNK_SIZE;
numChunks = length / DMA_MAX_CHUNK_SIZE;
// Make sure the length is an exact multiple of 16 words, as otherwise
// the last chunk would be dropped (the DMA unit does not support
// "incomplete" chunks). Note that this will impose limitations on the
// size of VRAM uploads.
assert(!(length % DMA_MAX_CHUNK_SIZE));
}
// Put the GPU into VRAM upload mode by sending the appropriate GP0 command
// and our coordinates.
waitForGP0Ready();
GPU_GP0 = gp0_vramWrite();
GPU_GP0 = gp0_xy(x, y);
GPU_GP0 = gp0_xy(width, height);
// Give DMA a pointer to the beginning of the data and tell it to send it in
// slice (chunked) mode.
DMA_MADR(DMA_GPU) = (uint32_t) data;
DMA_BCR (DMA_GPU) = chunkSize | (numChunks << 16);
DMA_CHCR(DMA_GPU) = 0
| DMA_CHCR_WRITE
| DMA_CHCR_MODE_SLICE
| DMA_CHCR_ENABLE;
}
#define GPU_CHAIN_BUFFER_SIZE 1024
typedef struct {
uint32_t data[GPU_CHAIN_BUFFER_SIZE];
uint32_t *nextPacket;
} GPUDMAChain;
static uint32_t *allocateGP0Packet(GPUDMAChain *chain, int numCommands) {
assert((numCommands >= 0) && (numCommands <= DMA_MAX_CHUNK_SIZE));
uint32_t *ptr = chain->nextPacket;
chain->nextPacket += numCommands + 1;
*ptr = gp0_tag(numCommands, chain->nextPacket);
assert(chain->nextPacket < &(chain->data)[GPU_CHAIN_BUFFER_SIZE]);
return &ptr[1];
}
// Once our texture has been uploaded to VRAM, we are going to save the metadata
// required to use it for drawing into this structure.
typedef struct {
uint8_t u, v;
uint16_t width, height;
uint16_t page;
} TextureInfo;
static void uploadTexture(
TextureInfo *info,
const void *data,
int x,
int y,
int width,
int height
) {
// Make sure the texture's size is valid. The GPU does not support textures
// larger than 256x256 pixels.
assert((width <= 256) && (height <= 256));
// Upload the texture to VRAM, wait for the process to complete and flush
// any previously used texture from the GPU's internal cache.
sendVRAMData(data, x, y, width, height);
waitForGPUDMADone();
GPU_GP0 = gp0_flushCache();
// Update the "texture page" attribute, a 16-bit field telling the GPU
// several details about the texture such as which 64x256 page it can be
// found in, its color depth and how semitransparent pixels shall be
// blended.
info->page = gp0_page(
x / 64,
y / 256,
GP0_BLEND_SEMITRANS,
GP0_COLOR_16BPP
);
// Calculate the texture's UV coordinates, i.e. its X/Y coordinates relative
// to the top left corner of the texture page.
info->u = (uint8_t) (x % 64);
info->v = (uint8_t) (y % 256);
info->width = (uint16_t) width;
info->height = (uint16_t) height;
}
#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 240
#define TEXTURE_WIDTH 32
#define TEXTURE_HEIGHT 32
// We're going to convert our texture into raw binary data using a Python script
// and embed it into this extern array through CMake. See CMakeLists.txt for
// more details.
extern const uint8_t textureData[];
int main(int argc, const char **argv) {
initSerialIO(115200);
if ((GPU_GP1 & GP1_STAT_FB_MODE_BITMASK) == GP1_STAT_FB_MODE_PAL) {
puts("Using PAL mode");
setupGPU(GP1_MODE_PAL, SCREEN_WIDTH, SCREEN_HEIGHT);
} else {
puts("Using NTSC mode");
setupGPU(GP1_MODE_NTSC, SCREEN_WIDTH, SCREEN_HEIGHT);
}
// Load the texture, placing it next to the two framebuffers in VRAM.
TextureInfo texture;
uploadTexture(
&texture,
textureData,
SCREEN_WIDTH * 2,
0,
TEXTURE_WIDTH,
TEXTURE_HEIGHT
);
int x = 0, velocityX = 1;
int y = 0, velocityY = 1;
GPUDMAChain dmaChains[2];
bool usingSecondFrame = false;
for (;;) {
int bufferX = usingSecondFrame ? SCREEN_WIDTH : 0;
int bufferY = 0;
GPUDMAChain *chain = &dmaChains[usingSecondFrame];
usingSecondFrame = !usingSecondFrame;
uint32_t *ptr;
GPU_GP1 = gp1_fbOffset(bufferX, bufferY);
chain->nextPacket = chain->data;
ptr = allocateGP0Packet(chain, 4);
ptr[0] = gp0_setPage(0, true, false);
ptr[1] = gp0_fbOffset1(bufferX, bufferY);
ptr[2] = gp0_fbOffset2(
bufferX + SCREEN_WIDTH - 1,
bufferY + SCREEN_HEIGHT - 1
);
ptr[3] = gp0_fbOrigin(bufferX, bufferY);
ptr = allocateGP0Packet(chain, 3);
ptr[0] = gp0_rgb(64, 64, 64) | gp0_vramFill();
ptr[1] = gp0_xy(bufferX, bufferY);
ptr[2] = gp0_xy(SCREEN_WIDTH, SCREEN_HEIGHT);
// Use the texture we uploaded to draw a sprite (textured rectangle).
// Two separate commands have to be sent: a texture page command to
// apply our page attribute and disable dithering, followed by the
// actual rectangle drawing command. Any subsequent rectangle commands
// will reuse the last page set, so it's not strictly necessary to send
// a page command for each rectangle drawn.
// NOTE: while not covered here, triangle and quad commands have an
// inline page attribute and do not require a separate page setting
// command (if not to toggle dithering, which the inline page field does
// not affect).
ptr = allocateGP0Packet(chain, 5);
ptr[0] = gp0_setPage(texture.page, false, false);
ptr[1] = gp0_rectangle(true, true, false);
ptr[2] = gp0_xy(x, y);
ptr[3] = gp0_uv(texture.u, texture.v, 0);
ptr[4] = gp0_xy(texture.width, texture.height);
*(chain->nextPacket) = gp0_endTag(0);
x += velocityX;
y += velocityY;
if ((x <= 0) || (x >= (SCREEN_WIDTH - texture.width)))
velocityX = -velocityX;
if ((y <= 0) || (y >= (SCREEN_HEIGHT - texture.height)))
velocityY = -velocityY;
waitForGP0Ready();
waitForVSync();
sendGPULinkedList(chain->data);
}
return 0;
}
================================================
FILE: src/05_palettes/main.c
================================================
/*
* ps1-bare-metal - (C) 2023-2025 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
/*
* This is a version of the previous example modified to use an indexed color
* texture instead of a raw one. The idea behind indexed color images is
* remarkably simple: by limiting the maximum number of unique colors in an
* image and storing their values separately, it is possible to reduce the size
* of the image data by replacing each pixel with an index to its color into the
* so-called CLUT (color lookup table) or palette.
*
* The PS1's GPU supports two indexed color formats: 4 bits per pixel (up to 16
* colors) and 8 bits per pixel (up to 256 colors). 4bpp and 8bpp textures are
* stored in VRAM "squished" horizontally, taking up half or a quarter of the
* size of an equivalent 16bpp texture respectively. Palettes are simply 16x1 or
* 256x1 16bpp images that can be placed anywhere in VRAM, with some minimal
* restrictions on alignment (their X coordinate must be a multiple of 16). This
* example shows how to upload a palette to VRAM alongside the image and set the
* appropriate GP0 attributes in order to let the GPU find and use it.
*/
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include "ps1/gpucmd.h"
#include "ps1/registers.h"
static void setupGPU(GP1VideoMode mode, int width, int height) {
int x = 0x760;
int y = (mode == GP1_MODE_PAL) ? 0xa3 : 0x88;
GP1HorizontalRes horizontalRes = GP1_HRES_320;
GP1VerticalRes verticalRes = GP1_VRES_256;
int offsetX = (width * gp1_clockMultiplierH(horizontalRes)) / 2;
int offsetY = (height / gp1_clockDividerV(verticalRes)) / 2;
GPU_GP1 = gp1_resetGPU();
GPU_GP1 = gp1_fbRangeH(x - offsetX, x + offsetX);
GPU_GP1 = gp1_fbRangeV(y - offsetY, y + offsetY);
GPU_GP1 = gp1_fbMode(
horizontalRes,
verticalRes,
mode,
false,
GP1_COLOR_16BPP
);
GPU_GP1 = gp1_dispBlank(false);
DMA_DPCR |= DMA_DPCR_CH_ENABLE(DMA_GPU);
DMA_CHCR(DMA_GPU) = 0;
GPU_GP1 = gp1_dmaRequestMode(GP1_DREQ_GP0_WRITE);
}
static void waitForGP0Ready(void) {
while (!(GPU_GP1 & GP1_STAT_CMD_READY))
__asm__ volatile("");
}
static void waitForGPUDMADone(void) {
while (DMA_CHCR(DMA_GPU) & DMA_CHCR_ENABLE)
__asm__ volatile("");
}
static void waitForVSync(void) {
while (!(IRQ_STAT & (1 << IRQ_VSYNC)))
__asm__ volatile("");
IRQ_STAT = ~(1 << IRQ_VSYNC);
}
static void sendGPULinkedList(const void *data) {
waitForGPUDMADone();
assert(!((uint32_t) data % 4));
DMA_MADR(DMA_GPU) = (uint32_t) data;
DMA_CHCR(DMA_GPU) = 0
| DMA_CHCR_WRITE
| DMA_CHCR_MODE_LIST
| DMA_CHCR_ENABLE;
}
#define DMA_MAX_CHUNK_SIZE 16
static void sendVRAMData(
const void *data,
int x,
int y,
int width,
int height
) {
waitForGPUDMADone();
assert(!((uint32_t) data % 4));
size_t length = (width * height + 1) / 2;
size_t chunkSize, numChunks;
if (length < DMA_MAX_CHUNK_SIZE) {
chunkSize = length;
numChunks = 1;
} else {
chunkSize = DMA_MAX_CHUNK_SIZE;
numChunks = length / DMA_MAX_CHUNK_SIZE;
assert(!(length % DMA_MAX_CHUNK_SIZE));
}
waitForGP0Ready();
GPU_GP0 = gp0_vramWrite();
GPU_GP0 = gp0_xy(x, y);
GPU_GP0 = gp0_xy(width, height);
DMA_MADR(DMA_GPU) = (uint32_t) data;
DMA_BCR (DMA_GPU) = chunkSize | (numChunks << 16);
DMA_CHCR(DMA_GPU) = 0
| DMA_CHCR_WRITE
| DMA_CHCR_MODE_SLICE
| DMA_CHCR_ENABLE;
}
#define GPU_CHAIN_BUFFER_SIZE 1024
typedef struct {
uint32_t data[GPU_CHAIN_BUFFER_SIZE];
uint32_t *nextPacket;
} GPUDMAChain;
static uint32_t *allocateGP0Packet(GPUDMAChain *chain, int numCommands) {
assert((numCommands >= 0) && (numCommands <= DMA_MAX_CHUNK_SIZE));
uint32_t *ptr = chain->nextPacket;
chain->nextPacket += numCommands + 1;
*ptr = gp0_tag(numCommands, chain->nextPacket);
assert(chain->nextPacket < &(chain->data)[GPU_CHAIN_BUFFER_SIZE]);
return &ptr[1];
}
// We need to add a new entry to this structure to store the CLUT attribute,
// another 16-bit field which will contain the coordinates of our texture's
// palette within VRAM.
typedef struct {
uint8_t u, v;
uint16_t width, height;
uint16_t page, clut;
} TextureInfo;
static void uploadIndexedTexture(
TextureInfo *info,
const void *image,
const void *palette,
int imageX,
int imageY,
int paletteX,
int paletteY,
int width,
int height,
GP0ColorDepth colorDepth
) {
assert((width <= 256) && (height <= 256));
// Determine how large the palette is and by which factor the image is
// squished horizontally in VRAM from the color depth.
int numColors = (colorDepth == GP0_COLOR_8BPP) ? 256 : 16;
int widthDivider = (colorDepth == GP0_COLOR_8BPP) ? 2 : 4;
// Make sure the palette is aligned correctly within VRAM and does not
// exceed its bounds.
assert(!(paletteX % 16) && ((paletteX + numColors) <= 1024));
// Upload the image and palette data separately, then flush any previously
// used texture from the GPU's internal cache.
sendVRAMData(image, imageX, imageY, width / widthDivider, height);
waitForGPUDMADone();
sendVRAMData(palette, paletteX, paletteY, numColors, 1);
waitForGPUDMADone();
GPU_GP0 = gp0_flushCache();
// Update the texture page and CLUT attributes to match the VRAM locations
// of the image and palette respectively.
info->page = gp0_page(
imageX / 64,
imageY / 256,
GP0_BLEND_SEMITRANS,
colorDepth
);
info->clut = gp0_clut(paletteX / 16, paletteY);
// UV coordinate calculation is slightly more complex than before. The GPU
// expects coordinates to be in texture pixels rather than VRAM pixels, so
// the U coordinate has to be multiplied by the previously computed divider.
info->u = (uint8_t) ((imageX % 64) * widthDivider);
info->v = (uint8_t) (imageY % 256);
info->width = (uint16_t) width;
info->height = (uint16_t) height;
}
#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 240
#define TEXTURE_WIDTH 32
#define TEXTURE_HEIGHT 32
#define TEXTURE_COLOR_DEPTH GP0_COLOR_4BPP
// The Python script will generate two separate files containing the image and
// palette data respectively, so we're going to embed both into the executable.
extern const uint8_t textureData[], paletteData[];
int main(int argc, const char **argv) {
initSerialIO(115200);
if ((GPU_GP1 & GP1_STAT_FB_MODE_BITMASK) == GP1_STAT_FB_MODE_PAL) {
puts("Using PAL mode");
setupGPU(GP1_MODE_PAL, SCREEN_WIDTH, SCREEN_HEIGHT);
} else {
puts("Using NTSC mode");
setupGPU(GP1_MODE_NTSC, SCREEN_WIDTH, SCREEN_HEIGHT);
}
// Load the texture, placing the image next to the two framebuffers in VRAM
// and the palette below the image.
TextureInfo texture;
uploadIndexedTexture(
&texture,
textureData,
paletteData,
SCREEN_WIDTH * 2,
0,
SCREEN_WIDTH * 2,
TEXTURE_HEIGHT,
TEXTURE_WIDTH,
TEXTURE_HEIGHT,
TEXTURE_COLOR_DEPTH
);
int x = 0, velocityX = 1;
int y = 0, velocityY = 1;
GPUDMAChain dmaChains[2];
bool usingSecondFrame = false;
for (;;) {
int bufferX = usingSecondFrame ? SCREEN_WIDTH : 0;
int bufferY = 0;
GPUDMAChain *chain = &dmaChains[usingSecondFrame];
usingSecondFrame = !usingSecondFrame;
uint32_t *ptr;
GPU_GP1 = gp1_fbOffset(bufferX, bufferY);
chain->nextPacket = chain->data;
ptr = allocateGP0Packet(chain, 4);
ptr[0] = gp0_setPage(0, true, false);
ptr[1] = gp0_fbOffset1(bufferX, bufferY);
ptr[2] = gp0_fbOffset2(
bufferX + SCREEN_WIDTH - 1,
bufferY + SCREEN_HEIGHT - 1
);
ptr[3] = gp0_fbOrigin(bufferX, bufferY);
ptr = allocateGP0Packet(chain, 3);
ptr[0] = gp0_rgb(64, 64, 64) | gp0_vramFill();
ptr[1] = gp0_xy(bufferX, bufferY);
ptr[2] = gp0_xy(SCREEN_WIDTH, SCREEN_HEIGHT);
// Draw the sprite, almost identically to how we did it in the previous
// example. Notice how the CLUT attribute is being passed to the GPU.
ptr = allocateGP0Packet(chain, 5);
ptr[0] = gp0_setPage(texture.page, false, false);
ptr[1] = gp0_rectangle(true, true, false);
ptr[2] = gp0_xy(x, y);
ptr[3] = gp0_uv(texture.u, texture.v, texture.clut);
ptr[4] = gp0_xy(texture.width, texture.height);
*(chain->nextPacket) = gp0_endTag(0);
x += velocityX;
y += velocityY;
if ((x <= 0) || (x >= (SCREEN_WIDTH - texture.width)))
velocityX = -velocityX;
if ((y <= 0) || (y >= (SCREEN_HEIGHT - texture.height)))
velocityY = -velocityY;
waitForGP0Ready();
waitForVSync();
sendGPULinkedList(chain->data);
}
return 0;
}
================================================
FILE: src/06_fonts/gpu.c
================================================
/*
* ps1-bare-metal - (C) 2023-2025 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include "gpu.h"
#include "ps1/gpucmd.h"
#include "ps1/registers.h"
#define DMA_MAX_CHUNK_SIZE 16
void setupGPU(GP1VideoMode mode, int width, int height) {
int x = 0x760;
int y = (mode == GP1_MODE_PAL) ? 0xa3 : 0x88;
GP1HorizontalRes horizontalRes = GP1_HRES_320;
GP1VerticalRes verticalRes = GP1_VRES_256;
int offsetX = (width * gp1_clockMultiplierH(horizontalRes)) / 2;
int offsetY = (height / gp1_clockDividerV(verticalRes)) / 2;
GPU_GP1 = gp1_resetGPU();
GPU_GP1 = gp1_fbRangeH(x - offsetX, x + offsetX);
GPU_GP1 = gp1_fbRangeV(y - offsetY, y + offsetY);
GPU_GP1 = gp1_fbMode(
horizontalRes,
verticalRes,
mode,
false,
GP1_COLOR_16BPP
);
GPU_GP1 = gp1_dispBlank(false);
DMA_DPCR |= DMA_DPCR_CH_ENABLE(DMA_GPU);
DMA_CHCR(DMA_GPU) = 0;
GPU_GP1 = gp1_dmaRequestMode(GP1_DREQ_GP0_WRITE);
}
void waitForGP0Ready(void) {
while (!(GPU_GP1 & GP1_STAT_CMD_READY))
__asm__ volatile("");
}
void waitForGPUDMADone(void) {
while (DMA_CHCR(DMA_GPU) & DMA_CHCR_ENABLE)
__asm__ volatile("");
}
void waitForVSync(void) {
while (!(IRQ_STAT & (1 << IRQ_VSYNC)))
__asm__ volatile("");
IRQ_STAT = ~(1 << IRQ_VSYNC);
}
void sendGPULinkedList(const void *data) {
waitForGPUDMADone();
assert(!((uint32_t) data % 4));
DMA_MADR(DMA_GPU) = (uint32_t) data;
DMA_CHCR(DMA_GPU) = 0
| DMA_CHCR_WRITE
| DMA_CHCR_MODE_LIST
| DMA_CHCR_ENABLE;
}
void sendVRAMData(
const void *data,
int x,
int y,
int width,
int height
) {
waitForGPUDMADone();
assert(!((uint32_t) data % 4));
size_t length = (width * height + 1) / 2;
size_t chunkSize, numChunks;
if (length < DMA_MAX_CHUNK_SIZE) {
chunkSize = length;
numChunks = 1;
} else {
chunkSize = DMA_MAX_CHUNK_SIZE;
numChunks = length / DMA_MAX_CHUNK_SIZE;
assert(!(length % DMA_MAX_CHUNK_SIZE));
}
waitForGP0Ready();
GPU_GP0 = gp0_vramWrite();
GPU_GP0 = gp0_xy(x, y);
GPU_GP0 = gp0_xy(width, height);
DMA_MADR(DMA_GPU) = (uint32_t) data;
DMA_BCR (DMA_GPU) = chunkSize | (numChunks << 16);
DMA_CHCR(DMA_GPU) = 0
| DMA_CHCR_WRITE
| DMA_CHCR_MODE_SLICE
| DMA_CHCR_ENABLE;
}
uint32_t *allocateGP0Packet(GPUDMAChain *chain, int numCommands) {
assert((numCommands >= 0) && (numCommands <= DMA_MAX_CHUNK_SIZE));
uint32_t *ptr = chain->nextPacket;
chain->nextPacket += numCommands + 1;
*ptr = gp0_tag(numCommands, chain->nextPacket);
assert(chain->nextPacket < &(chain->data)[GPU_CHAIN_BUFFER_SIZE]);
return &ptr[1];
}
void uploadTexture(
TextureInfo *info,
const void *data,
int x,
int y,
int width,
int height
) {
assert((width <= 256) && (height <= 256));
sendVRAMData(data, x, y, width, height);
waitForGPUDMADone();
GPU_GP0 = gp0_flushCache();
info->page = gp0_page(
x / 64,
y / 256,
GP0_BLEND_SEMITRANS,
GP0_COLOR_16BPP
);
info->clut = 0;
info->u = (uint8_t) (x % 64);
info->v = (uint8_t) (y % 256);
info->width = (uint16_t) width;
info->height = (uint16_t) height;
}
void uploadIndexedTexture(
TextureInfo *info,
const void *image,
const void *palette,
int imageX,
int imageY,
int paletteX,
int paletteY,
int width,
int height,
GP0ColorDepth colorDepth
) {
assert((width <= 256) && (height <= 256));
int numColors = (colorDepth == GP0_COLOR_8BPP) ? 256 : 16;
int widthDivider = (colorDepth == GP0_COLOR_8BPP) ? 2 : 4;
assert(!(paletteX % 16) && ((paletteX + numColors) <= 1024));
sendVRAMData(image, imageX, imageY, width / widthDivider, height);
waitForGPUDMADone();
sendVRAMData(palette, paletteX, paletteY, numColors, 1);
waitForGPUDMADone();
GPU_GP0 = gp0_flushCache();
info->page = gp0_page(
imageX / 64,
imageY / 256,
GP0_BLEND_SEMITRANS,
colorDepth
);
info->clut = gp0_clut(paletteX / 16, paletteY);
info->u = (uint8_t) ((imageX % 64) * widthDivider);
info->v = (uint8_t) (imageY % 256);
info->width = (uint16_t) width;
info->height = (uint16_t) height;
}
================================================
FILE: src/06_fonts/gpu.h
================================================
/*
* ps1-bare-metal - (C) 2023-2025 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
#pragma once
#include <stdint.h>
#include "ps1/gpucmd.h"
#define GPU_CHAIN_BUFFER_SIZE 1024
typedef struct {
uint32_t data[GPU_CHAIN_BUFFER_SIZE];
uint32_t *nextPacket;
} GPUDMAChain;
typedef struct {
uint8_t u, v;
uint16_t width, height;
uint16_t page, clut;
} TextureInfo;
#ifdef __cplusplus
extern "C" {
#endif
void setupGPU(GP1VideoMode mode, int width, int height);
void waitForGP0Ready(void);
void waitForGPUDMADone(void);
void waitForVSync(void);
void sendGPULinkedList(const void *data);
void sendVRAMData(
const void *data,
int x,
int y,
int width,
int height
);
uint32_t *allocateGP0Packet(GPUDMAChain *chain, int numCommands);
void uploadTexture(
TextureInfo *info,
const void *data,
int x,
int y,
int width,
int height
);
void uploadIndexedTexture(
TextureInfo *info,
const void *image,
const void *palette,
int imageX,
int imageY,
int paletteX,
int paletteY,
int width,
int height,
GP0ColorDepth colorDepth
);
#ifdef __cplusplus
}
#endif
================================================
FILE: src/06_fonts/main.c
================================================
/*
* ps1-bare-metal - (C) 2023-2025 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
/*
* We saw how to load a single texture and display it in the last two examples.
* Textures, however, are not always simple images displayed in their entirety:
* sometimes they hold more than one image (e.g. all frames of a character's
* animation in a 2D game) but are "cropped out" on the fly during rendering to
* only draw a single frame at a time. These textures are known as spritesheets
* and the PS1's GPU fully supports them, as it allows for arbitrary UV
* coordinates to be used.
*
* This example is going to show how to implement a simple font system for text
* rendering, since that's one of the most common use cases for spritesheets. We
* are going to load a single texture containing all our font's characters, as
* having hundreds of tiny textures for each character would be extremely
* inefficient, and then use a lookup table to obtain the UV coordinates, width
* and height of each character in a string.
*
* NOTE: in order to make the code easier to read, I have moved all the
* GPU-related functions from previous examples to a separate source file.
*/
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include "gpu.h"
#include "ps1/gpucmd.h"
#include "ps1/registers.h"
// In order to pick sprites (characters) out of our spritesheet, we need a table
// listing all of them (in ASCII order in this case) with their UV coordinates
// within the sheet as well as their dimensions. In this example we're going to
// hardcode the table, however in an actual game you may want to store this data
// in the same file as the image and palette data.
typedef struct {
uint8_t x, y, width, height;
} SpriteInfo;
static const SpriteInfo fontSprites[] = {
{ .x = 6, .y = 0, .width = 2, .height = 9 }, // !
{ .x = 12, .y = 0, .width = 4, .height = 9 }, // "
{ .x = 18, .y = 0, .width = 6, .height = 9 }, // #
{ .x = 24, .y = 0, .width = 6, .height = 9 }, // $
{ .x = 30, .y = 0, .width = 6, .height = 9 }, // %
{ .x = 36, .y = 0, .width = 6, .height = 9 }, // &
{ .x = 42, .y = 0, .width = 2, .height = 9 }, // '
{ .x = 48, .y = 0, .width = 3, .height = 9 }, // (
{ .x = 54, .y = 0, .width = 3, .height = 9 }, // )
{ .x = 60, .y = 0, .width = 4, .height = 9 }, // *
{ .x = 66, .y = 0, .width = 6, .height = 9 }, // +
{ .x = 72, .y = 0, .width = 3, .height = 9 }, // ,
{ .x = 78, .y = 0, .width = 6, .height = 9 }, // -
{ .x = 84, .y = 0, .width = 2, .height = 9 }, // .
{ .x = 90, .y = 0, .width = 6, .height = 9 }, // /
{ .x = 0, .y = 9, .width = 6, .height = 9 }, // 0
{ .x = 6, .y = 9, .width = 6, .height = 9 }, // 1
{ .x = 12, .y = 9, .width = 6, .height = 9 }, // 2
{ .x = 18, .y = 9, .width = 6, .height = 9 }, // 3
{ .x = 24, .y = 9, .width = 6, .height = 9 }, // 4
{ .x = 30, .y = 9, .width = 6, .height = 9 }, // 5
{ .x = 36, .y = 9, .width = 6, .height = 9 }, // 6
{ .x = 42, .y = 9, .width = 6, .height = 9 }, // 7
{ .x = 48, .y = 9, .width = 6, .height = 9 }, // 8
{ .x = 54, .y = 9, .width = 6, .height = 9 }, // 9
{ .x = 60, .y = 9, .width = 2, .height = 9 }, // :
{ .x = 66, .y = 9, .width = 3, .height = 9 }, // ;
{ .x = 72, .y = 9, .width = 6, .height = 9 }, // <
{ .x = 78, .y = 9, .width = 6, .height = 9 }, // =
{ .x = 84, .y = 9, .width = 6, .height = 9 }, // >
{ .x = 90, .y = 9, .width = 6, .height = 9 }, // ?
{ .x = 0, .y = 18, .width = 6, .height = 9 }, // @
{ .x = 6, .y = 18, .width = 6, .height = 9 }, // A
{ .x = 12, .y = 18, .width = 6, .height = 9 }, // B
{ .x = 18, .y = 18, .width = 6, .height = 9 }, // C
{ .x = 24, .y = 18, .width = 6, .height = 9 }, // D
{ .x = 30, .y = 18, .width = 6, .height = 9 }, // E
{ .x = 36, .y = 18, .width = 6, .height = 9 }, // F
{ .x = 42, .y = 18, .width = 6, .height = 9 }, // G
{ .x = 48, .y = 18, .width = 6, .height = 9 }, // H
{ .x = 54, .y = 18, .width = 4, .height = 9 }, // I
{ .x = 60, .y = 18, .width = 5, .height = 9 }, // J
{ .x = 66, .y = 18, .width = 6, .height = 9 }, // K
{ .x = 72, .y = 18, .width = 6, .height = 9 }, // L
{ .x = 78, .y = 18, .width = 6, .height = 9 }, // M
{ .x = 84, .y = 18, .width = 6, .height = 9 }, // N
{ .x = 90, .y = 18, .width = 6, .height = 9 }, // O
{ .x = 0, .y = 27, .width = 6, .height = 9 }, // P
{ .x = 6, .y = 27, .width = 6, .height = 9 }, // Q
{ .x = 12, .y = 27, .width = 6, .height = 9 }, // R
{ .x = 18, .y = 27, .width = 6, .height = 9 }, // S
{ .x = 24, .y = 27, .width = 6, .height = 9 }, // T
{ .x = 30, .y = 27, .width = 6, .height = 9 }, // U
{ .x = 36, .y = 27, .width = 6, .height = 9 }, // V
{ .x = 42, .y = 27, .width = 6, .height = 9 }, // W
{ .x = 48, .y = 27, .width = 6, .height = 9 }, // X
{ .x = 54, .y = 27, .width = 6, .height = 9 }, // Y
{ .x = 60, .y = 27, .width = 6, .height = 9 }, // Z
{ .x = 66, .y = 27, .width = 3, .height = 9 }, // [
{ .x = 72, .y = 27, .width = 6, .height = 9 }, // Backslash
{ .x = 78, .y = 27, .width = 3, .height = 9 }, // ]
{ .x = 84, .y = 27, .width = 4, .height = 9 }, // ^
{ .x = 90, .y = 27, .width = 6, .height = 9 }, // _
{ .x = 0, .y = 36, .width = 3, .height = 9 }, // `
{ .x = 6, .y = 36, .width = 6, .height = 9 }, // a
{ .x = 12, .y = 36, .width = 6, .height = 9 }, // b
{ .x = 18, .y = 36, .width = 6, .height = 9 }, // c
{ .x = 24, .y = 36, .width = 6, .height = 9 }, // d
{ .x = 30, .y = 36, .width = 6, .height = 9 }, // e
{ .x = 36, .y = 36, .width = 5, .height = 9 }, // f
{ .x = 42, .y = 36, .width = 6, .height = 9 }, // g
{ .x = 48, .y = 36, .width = 5, .height = 9 }, // h
{ .x = 54, .y = 36, .width = 2, .height = 9 }, // i
{ .x = 60, .y = 36, .width = 4, .height = 9 }, // j
{ .x = 66, .y = 36, .width = 5, .height = 9 }, // k
{ .x = 72, .y = 36, .width = 2, .height = 9 }, // l
{ .x = 78, .y = 36, .width = 6, .height = 9 }, // m
{ .x = 84, .y = 36, .width = 5, .height = 9 }, // n
{ .x = 90, .y = 36, .width = 6, .height = 9 }, // o
{ .x = 0, .y = 45, .width = 6, .height = 9 }, // p
{ .x = 6, .y = 45, .width = 6, .height = 9 }, // q
{ .x = 12, .y = 45, .width = 6, .height = 9 }, // r
{ .x = 18, .y = 45, .width = 6, .height = 9 }, // s
{ .x = 24, .y = 45, .width = 5, .height = 9 }, // t
{ .x = 30, .y = 45, .width = 5, .height = 9 }, // u
{ .x = 36, .y = 45, .width = 6, .height = 9 }, // v
{ .x = 42, .y = 45, .width = 6, .height = 9 }, // w
{ .x = 48, .y = 45, .width = 6, .height = 9 }, // x
{ .x = 54, .y = 45, .width = 6, .height = 9 }, // y
{ .x = 60, .y = 45, .width = 5, .height = 9 }, // z
{ .x = 66, .y = 45, .width = 4, .height = 9 }, // {
{ .x = 72, .y = 45, .width = 2, .height = 9 }, // |
{ .x = 78, .y = 45, .width = 4, .height = 9 }, // }
{ .x = 84, .y = 45, .width = 6, .height = 9 }, // ~
{ .x = 90, .y = 45, .width = 6, .height = 9 } // Invalid character
};
#define FONT_FIRST_TABLE_CHAR '!'
#define FONT_SPACE_WIDTH 4
#define FONT_TAB_WIDTH 32
#define FONT_LINE_HEIGHT 10
static void printString(
GPUDMAChain *chain,
const TextureInfo *font,
int x,
int y,
const char *str
) {
int currentX = x, currentY = y;
uint32_t *ptr;
// Start by sending a texture page command to tell the GPU to use the font's
// spritesheet. The page setting persists when drawing rectangles, so
// sending it here just once is enough.
ptr = allocateGP0Packet(chain, 1);
ptr[0] = gp0_setPage(font->page, false, false);
// Iterate over every character in the string.
for (; *str; str++) {
char ch = *str;
// Check if the character is "special" and shall be handled without
// drawing any sprite, or if it's invalid and should be rendered as a
// box with a question mark (character code 127).
switch (ch) {
case '\t':
currentX += FONT_TAB_WIDTH - 1;
currentX -= currentX % FONT_TAB_WIDTH;
continue;
case '\n':
currentX = x;
currentY += FONT_LINE_HEIGHT;
continue;
case ' ':
currentX += FONT_SPACE_WIDTH;
continue;
case '\x80' ... '\xff':
ch = '\x7f';
break;
}
// If the character was not a tab, newline or space, fetch its
// respective entry from the sprite coordinate table.
const SpriteInfo *sprite = &fontSprites[ch - FONT_FIRST_TABLE_CHAR];
// Draw the character, summing the UV coordinates of the spritesheet in
// VRAM to those of the sprite itself within the sheet. Enable blending
// to make sure any semitransparent pixels in the font get rendered
// correctly.
ptr = allocateGP0Packet(chain, 4);
ptr[0] = gp0_rectangle(true, true, true);
ptr[1] = gp0_xy(currentX, currentY);
ptr[2] = gp0_uv(font->u + sprite->x, font->v + sprite->y, font->clut);
ptr[3] = gp0_xy(sprite->width, sprite->height);
// Move onto the next character.
currentX += sprite->width;
}
}
#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 240
#define FONT_WIDTH 96
#define FONT_HEIGHT 56
#define FONT_COLOR_DEPTH GP0_COLOR_4BPP
extern const uint8_t fontTexture[], fontPalette[];
int main(int argc, const char **argv) {
initSerialIO(115200);
if ((GPU_GP1 & GP1_STAT_FB_MODE_BITMASK) == GP1_STAT_FB_MODE_PAL) {
puts("Using PAL mode");
setupGPU(GP1_MODE_PAL, SCREEN_WIDTH, SCREEN_HEIGHT);
} else {
puts("Using NTSC mode");
setupGPU(GP1_MODE_NTSC, SCREEN_WIDTH, SCREEN_HEIGHT);
}
TextureInfo font;
uploadIndexedTexture(
&font,
fontTexture,
fontPalette,
SCREEN_WIDTH * 2,
0,
SCREEN_WIDTH * 2,
FONT_HEIGHT,
FONT_WIDTH,
FONT_HEIGHT,
FONT_COLOR_DEPTH
);
GPUDMAChain dmaChains[2];
bool usingSecondFrame = false;
int frameCounter = 0;
for (;;) {
int bufferX = usingSecondFrame ? SCREEN_WIDTH : 0;
int bufferY = 0;
GPUDMAChain *chain = &dmaChains[usingSecondFrame];
usingSecondFrame = !usingSecondFrame;
uint32_t *ptr;
GPU_GP1 = gp1_fbOffset(bufferX, bufferY);
chain->nextPacket = chain->data;
ptr = allocateGP0Packet(chain, 4);
ptr[0] = gp0_setPage(0, true, false);
ptr[1] = gp0_fbOffset1(bufferX, bufferY);
ptr[2] = gp0_fbOffset2(
bufferX + SCREEN_WIDTH - 1,
bufferY + SCREEN_HEIGHT - 1
);
ptr[3] = gp0_fbOrigin(bufferX, bufferY);
ptr = allocateGP0Packet(chain, 3);
ptr[0] = gp0_rgb(64, 64, 64) | gp0_vramFill();
ptr[1] = gp0_xy(bufferX, bufferY);
ptr[2] = gp0_xy(SCREEN_WIDTH, SCREEN_HEIGHT);
printString(
chain,
&font,
16,
32,
"Hello world!\n"
"We're printing text using nothing but our font spritesheet."
);
// Show the current frame number by formatting some text into a
// temporary buffer then printing it.
char buffer[32];
snprintf(buffer, sizeof(buffer), "Current frame: %d", frameCounter++);
printString(chain, &font, 16, 64, buffer);
*(chain->nextPacket) = gp0_endTag(0);
waitForGP0Ready();
waitForVSync();
sendGPULinkedList(chain->data);
}
return 0;
}
================================================
FILE: src/07_orderingTable/gpu.c
================================================
/*
* ps1-bare-metal - (C) 2023-2025 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include "gpu.h"
#include "ps1/gpucmd.h"
#include "ps1/registers.h"
#define DMA_MAX_CHUNK_SIZE 16
void setupGPU(GP1VideoMode mode, int width, int height) {
int x = 0x760;
int y = (mode == GP1_MODE_PAL) ? 0xa3 : 0x88;
GP1HorizontalRes horizontalRes = GP1_HRES_320;
GP1VerticalRes verticalRes = GP1_VRES_256;
int offsetX = (width * gp1_clockMultiplierH(horizontalRes)) / 2;
int offsetY = (height / gp1_clockDividerV(verticalRes)) / 2;
GPU_GP1 = gp1_resetGPU();
GPU_GP1 = gp1_fbRangeH(x - offsetX, x + offsetX);
GPU_GP1 = gp1_fbRangeV(y - offsetY, y + offsetY);
GPU_GP1 = gp1_fbMode(
horizontalRes,
verticalRes,
mode,
false,
GP1_COLOR_16BPP
);
GPU_GP1 = gp1_dispBlank(false);
// Enable and reset the OTC DMA channel in addition to the GPU one.
DMA_DPCR |= 0
| DMA_DPCR_CH_ENABLE(DMA_GPU)
| DMA_DPCR_CH_ENABLE(DMA_OTC);
DMA_CHCR(DMA_GPU) = 0;
DMA_CHCR(DMA_OTC) = 0;
GPU_GP1 = gp1_dmaRequestMode(GP1_DREQ_GP0_WRITE);
}
void waitForGP0Ready(void) {
while (!(GPU_GP1 & GP1_STAT_CMD_READY))
__asm__ volatile("");
}
void waitForGPUDMADone(void) {
while (DMA_CHCR(DMA_GPU) & DMA_CHCR_ENABLE)
__asm__ volatile("");
}
void waitForVSync(void) {
while (!(IRQ_STAT & (1 << IRQ_VSYNC)))
__asm__ volatile("");
IRQ_STAT = ~(1 << IRQ_VSYNC);
}
void sendGPULinkedList(const void *data) {
waitForGPUDMADone();
assert(!((uint32_t) data % 4));
DMA_MADR(DMA_GPU) = (uint32_t) data;
DMA_CHCR(DMA_GPU) = 0
| DMA_CHCR_WRITE
| DMA_CHCR_MODE_LIST
| DMA_CHCR_ENABLE;
}
void sendVRAMData(
const void *data,
int x,
int y,
int width,
int height
) {
waitForGPUDMADone();
assert(!((uint32_t) data % 4));
size_t length = (width * height + 1) / 2;
size_t chunkSize, numChunks;
if (length < DMA_MAX_CHUNK_SIZE) {
chunkSize = length;
numChunks = 1;
} else {
chunkSize = DMA_MAX_CHUNK_SIZE;
numChunks = length / DMA_MAX_CHUNK_SIZE;
assert(!(length % DMA_MAX_CHUNK_SIZE));
}
waitForGP0Ready();
GPU_GP0 = gp0_vramWrite();
GPU_GP0 = gp0_xy(x, y);
GPU_GP0 = gp0_xy(width, height);
DMA_MADR(DMA_GPU) = (uint32_t) data;
DMA_BCR (DMA_GPU) = chunkSize | (numChunks << 16);
DMA_CHCR(DMA_GPU) = 0
| DMA_CHCR_WRITE
| DMA_CHCR_MODE_SLICE
| DMA_CHCR_ENABLE;
}
void clearOrderingTable(uint32_t *table, int numEntries) {
// Set up the OTC DMA channel to transfer a new empty ordering table to RAM.
// The table is always reversed and generated "backwards" (the last item in
// the table is the first one that will be written), so we must give DMA a
// pointer to the end of the table rather than its beginning.
DMA_MADR(DMA_OTC) = (uint32_t) &table[numEntries - 1];
DMA_BCR (DMA_OTC) = numEntries;
DMA_CHCR(DMA_OTC) = 0
| DMA_CHCR_READ
| DMA_CHCR_REVERSE
| DMA_CHCR_MODE_BURST
| DMA_CHCR_ENABLE
| DMA_CHCR_TRIGGER;
// Wait for DMA to finish generating the table.
while (DMA_CHCR(DMA_OTC) & DMA_CHCR_ENABLE)
__asm__ volatile("");
}
// As we're using an ordering table, allocateGP0Packet() now takes the packet's
// Z index (i.e. the index of the "bucket" to link it to) as an argument. The
// table is reversed, so packets with higher Z values will be drawn first and
// between two packets with the same Z index the most recently added one will
// take precedence.
uint32_t *allocateGP0Packet(GPUDMAChain *chain, int zIndex, int numCommands) {
// Ensure both the packet length and index are within valid range.
assert((numCommands >= 0) && (numCommands <= DMA_MAX_CHUNK_SIZE));
assert((zIndex >= 0) && (zIndex < GPU_ORDERING_TABLE_SIZE));
uint32_t *ptr = chain->nextPacket;
chain->nextPacket += numCommands + 1;
// Splice the new packet into the ordering table by:
// - taking the address the ordering table entry currently points to;
// - replacing that address with a pointer to the packet;
// - linking the packet to the old address.
*ptr = gp0_tag(numCommands, (void *) chain->orderingTable[zIndex]);
chain->orderingTable[zIndex] = gp0_tag(0, ptr);
assert(chain->nextPacket < &(chain->data)[GPU_CHAIN_BUFFER_SIZE]);
return &ptr[1];
}
void uploadTexture(
TextureInfo *info,
const void *data,
int x,
int y,
int width,
int height
) {
assert((width <= 256) && (height <= 256));
sendVRAMData(data, x, y, width, height);
waitForGPUDMADone();
GPU_GP0 = gp0_flushCache();
info->page = gp0_page(
x / 64,
y / 256,
GP0_BLEND_SEMITRANS,
GP0_COLOR_16BPP
);
info->clut = 0;
info->u = (uint8_t) (x % 64);
info->v = (uint8_t) (y % 256);
info->width = (uint16_t) width;
info->height = (uint16_t) height;
}
void uploadIndexedTexture(
TextureInfo *info,
const void *image,
const void *palette,
int imageX,
int imageY,
int paletteX,
int paletteY,
int width,
int height,
GP0ColorDepth colorDepth
) {
assert((width <= 256) && (height <= 256));
int numColors = (colorDepth == GP0_COLOR_8BPP) ? 256 : 16;
int widthDivider = (colorDepth == GP0_COLOR_8BPP) ? 2 : 4;
assert(!(paletteX % 16) && ((paletteX + numColors) <= 1024));
sendVRAMData(image, imageX, imageY, width / widthDivider, height);
waitForGPUDMADone();
sendVRAMData(palette, paletteX, paletteY, numColors, 1);
waitForGPUDMADone();
GPU_GP0 = gp0_flushCache();
info->page = gp0_page(
imageX / 64,
imageY / 256,
GP0_BLEND_SEMITRANS,
colorDepth
);
info->clut = gp0_clut(paletteX / 16, paletteY);
info->u = (uint8_t) ((imageX % 64) * widthDivider);
info->v = (uint8_t) (imageY % 256);
info->width = (uint16_t) width;
info->height = (uint16_t) height;
}
================================================
FILE: src/07_orderingTable/gpu.h
================================================
/*
* ps1-bare-metal - (C) 2023-2025 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
#pragma once
#include <stdint.h>
#include "ps1/gpucmd.h"
// We are going to store the ordering table for each frame as part of the DMA
// chain structure. We'll have 32 different "buckets" and thus Z indices at our
// disposal.
#define GPU_CHAIN_BUFFER_SIZE 1024
#define GPU_ORDERING_TABLE_SIZE 32
typedef struct {
uint32_t data[GPU_CHAIN_BUFFER_SIZE];
uint32_t orderingTable[GPU_ORDERING_TABLE_SIZE];
uint32_t *nextPacket;
} GPUDMAChain;
typedef struct {
uint8_t u, v;
uint16_t width, height;
uint16_t page, clut;
} TextureInfo;
#ifdef __cplusplus
extern "C" {
#endif
void setupGPU(GP1VideoMode mode, int width, int height);
void waitForGP0Ready(void);
void waitForGPUDMADone(void);
void waitForVSync(void);
void sendGPULinkedList(const void *data);
void sendVRAMData(
const void *data,
int x,
int y,
int width,
int height
);
void clearOrderingTable(uint32_t *table, int numEntries);
uint32_t *allocateGP0Packet(GPUDMAChain *chain, int zIndex, int numCommands);
void uploadTexture(
TextureInfo *info,
const void *data,
int x,
int y,
int width,
int height
);
void uploadIndexedTexture(
TextureInfo *info,
const void *image,
const void *palette,
int imageX,
int imageY,
int paletteX,
int paletteY,
int width,
int height,
GP0ColorDepth colorDepth
);
#ifdef __cplusplus
}
#endif
================================================
FILE: src/07_orderingTable/main.c
================================================
/*
* ps1-bare-metal - (C) 2023-2025 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
/*
* We have seen pretty much every core feature of the PS1's GPU at this point.
* However, an important piece of functionality is still missing: we do not know
* how to control the order the GPU processes our linked list packets in. This
* may not sound particularly useful for 2D graphics but is crucial for 3D, as
* we'll have to sort our polygons by distance to make sure items closest to the
* camera are drawn last (the GPU has no depth buffer to help with this).
*
* Fortunately, linked lists lend themselves well to manipulation and sorting.
* The DMA unit, which we've only used for its GPU channel so far, includes
* another channel known as OTC, which can quickly generate a series of empty
* (header-only) GPU DMA packets linked to each other and write them to RAM.
* These packets will form what's known as an ordering table, a chain of dummy
* packets whose purpose is to serve as "anchor points" for other packets to be
* linked to. By having an ordering table with N items it is thus possible to
* have N different "buckets" to sort packets into, with the ordering table
* linking all buckets together and making sure the GPU draws them in order.
*/
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include "gpu.h"
#include "ps1/gpucmd.h"
#include "ps1/registers.h"
#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 240
int main(int argc, const char **argv) {
initSerialIO(115200);
if ((GPU_GP1 & GP1_STAT_FB_MODE_BITMASK) == GP1_STAT_FB_MODE_PAL) {
puts("Using PAL mode");
setupGPU(GP1_MODE_PAL, SCREEN_WIDTH, SCREEN_HEIGHT);
} else {
puts("Using NTSC mode");
setupGPU(GP1_MODE_NTSC, SCREEN_WIDTH, SCREEN_HEIGHT);
}
GPUDMAChain dmaChains[2];
bool usingSecondFrame = false;
int frameCounter = 0;
for (;;) {
int bufferX = usingSecondFrame ? SCREEN_WIDTH : 0;
int bufferY = 0;
GPUDMAChain *chain = &dmaChains[usingSecondFrame];
usingSecondFrame = !usingSecondFrame;
uint32_t *ptr;
GPU_GP1 = gp1_fbOffset(bufferX, bufferY);
// Reset the ordering table to a blank state.
clearOrderingTable(chain->orderingTable, GPU_ORDERING_TABLE_SIZE);
chain->nextPacket = chain->data;
// Draw 16 stacked squares, animating their Z indices. The packets are
// always allocated in the same order (top left to bottom right square),
// but the table will reorder them as they are sent to the GPU.
int x = 16, y = 24;
int frontSquareIndex = (frameCounter++ / 10) % 16;
for (int i = 0; i < 16; i++) {
uint32_t color = gp0_rgb(i * 15, i * 15, 0);
int zIndex;
if (i < frontSquareIndex)
zIndex = frontSquareIndex - i;
else
zIndex = i - frontSquareIndex;
ptr = allocateGP0Packet(chain, zIndex, 3);
ptr[0] = color | gp0_rectangle(false, false, false);
ptr[1] = gp0_xy(x, y);
ptr[2] = gp0_xy(32, 32);
x += 16;
y += 10;
}
// Place the framebuffer offset and screen clearing commands last, as
// the "furthest away" items in the table. Since the ordering table is
// reversed (see the allocateGP0Packet() note), this ensures they'll be
// executed first.
ptr = allocateGP0Packet(chain, GPU_ORDERING_TABLE_SIZE - 1, 3);
ptr[0] = gp0_rgb(64, 64, 64) | gp0_vramFill();
ptr[1] = gp0_xy(bufferX, bufferY);
ptr[2] = gp0_xy(SCREEN_WIDTH, SCREEN_HEIGHT);
ptr = allocateGP0Packet(chain, GPU_ORDERING_TABLE_SIZE - 1, 4);
ptr[0] = gp0_setPage(0, true, false);
ptr[1] = gp0_fbOffset1(bufferX, bufferY);
ptr[2] = gp0_fbOffset2(
bufferX + SCREEN_WIDTH - 1,
bufferY + SCREEN_HEIGHT - 1
);
ptr[3] = gp0_fbOrigin(bufferX, bufferY);
// Give DMA a pointer to the first (last) entry in the table. There is
// no need to terminate the table manually as the OTC DMA channel
// already inserts a terminator packet.
waitForGP0Ready();
waitForVSync();
sendGPULinkedList(&(chain->orderingTable)[GPU_ORDERING_TABLE_SIZE - 1]);
}
return 0;
}
================================================
FILE: src/08_spinningCube/gpu.c
================================================
/*
* ps1-bare-metal - (C) 2023-2025 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include "gpu.h"
#include "ps1/gpucmd.h"
#include "ps1/registers.h"
#define DMA_MAX_CHUNK_SIZE 16
void setupGPU(GP1VideoMode mode, int width, int height) {
int x = 0x760;
int y = (mode == GP1_MODE_PAL) ? 0xa3 : 0x88;
GP1HorizontalRes horizontalRes = GP1_HRES_320;
GP1VerticalRes verticalRes = GP1_VRES_256;
int offsetX = (width * gp1_clockMultiplierH(horizontalRes)) / 2;
int offsetY = (height / gp1_clockDividerV(verticalRes)) / 2;
GPU_GP1 = gp1_resetGPU();
GPU_GP1 = gp1_fbRangeH(x - offsetX, x + offsetX);
GPU_GP1 = gp1_fbRangeV(y - offsetY, y + offsetY);
GPU_GP1 = gp1_fbMode(
horizontalRes,
verticalRes,
mode,
false,
GP1_COLOR_16BPP
);
GPU_GP1 = gp1_dispBlank(false);
DMA_DPCR |= 0
| DMA_DPCR_CH_ENABLE(DMA_GPU)
| DMA_DPCR_CH_ENABLE(DMA_OTC);
DMA_CHCR(DMA_GPU) = 0;
DMA_CHCR(DMA_OTC) = 0;
GPU_GP1 = gp1_dmaRequestMode(GP1_DREQ_GP0_WRITE);
}
void waitForGP0Ready(void) {
while (!(GPU_GP1 & GP1_STAT_CMD_READY))
__asm__ volatile("");
}
void waitForGPUDMADone(void) {
while (DMA_CHCR(DMA_GPU) & DMA_CHCR_ENABLE)
__asm__ volatile("");
}
void waitForVSync(void) {
while (!(IRQ_STAT & (1 << IRQ_VSYNC)))
__asm__ volatile("");
IRQ_STAT = ~(1 << IRQ_VSYNC);
}
void sendGPULinkedList(const void *data) {
waitForGPUDMADone();
assert(!((uint32_t) data % 4));
DMA_MADR(DMA_GPU) = (uint32_t) data;
DMA_CHCR(DMA_GPU) = 0
| DMA_CHCR_WRITE
| DMA_CHCR_MODE_LIST
| DMA_CHCR_ENABLE;
}
void sendVRAMData(
const void *data,
int x,
int y,
int width,
int height
) {
waitForGPUDMADone();
assert(!((uint32_t) data % 4));
size_t length = (width * height + 1) / 2;
size_t chunkSize, numChunks;
if (length < DMA_MAX_CHUNK_SIZE) {
chunkSize = length;
numChunks = 1;
} else {
chunkSize = DMA_MAX_CHUNK_SIZE;
numChunks = length / DMA_MAX_CHUNK_SIZE;
assert(!(length % DMA_MAX_CHUNK_SIZE));
}
waitForGP0Ready();
GPU_GP0 = gp0_vramWrite();
GPU_GP0 = gp0_xy(x, y);
GPU_GP0 = gp0_xy(width, height);
DMA_MADR(DMA_GPU) = (uint32_t) data;
DMA_BCR (DMA_GPU) = chunkSize | (numChunks << 16);
DMA_CHCR(DMA_GPU) = 0
| DMA_CHCR_WRITE
| DMA_CHCR_MODE_SLICE
| DMA_CHCR_ENABLE;
}
void clearOrderingTable(uint32_t *table, int numEntries) {
DMA_MADR(DMA_OTC) = (uint32_t) &table[numEntries - 1];
DMA_BCR (DMA_OTC) = numEntries;
DMA_CHCR(DMA_OTC) = 0
| DMA_CHCR_READ
| DMA_CHCR_REVERSE
| DMA_CHCR_MODE_BURST
| DMA_CHCR_ENABLE
| DMA_CHCR_TRIGGER;
while (DMA_CHCR(DMA_OTC) & DMA_CHCR_ENABLE)
__asm__ volatile("");
}
uint32_t *allocateGP0Packet(GPUDMAChain *chain, int zIndex, int numCommands) {
assert((numCommands >= 0) && (numCommands <= DMA_MAX_CHUNK_SIZE));
assert((zIndex >= 0) && (zIndex < GPU_ORDERING_TABLE_SIZE));
uint32_t *ptr = chain->nextPacket;
chain->nextPacket += numCommands + 1;
*ptr = gp0_tag(numCommands, (void *) chain->orderingTable[zIndex]);
chain->orderingTable[zIndex] = gp0_tag(0, ptr);
assert(chain->nextPacket < &(chain->data)[GPU_CHAIN_BUFFER_SIZE]);
return &ptr[1];
}
void uploadTexture(
TextureInfo *info,
const void *data,
int x,
int y,
int width,
int height
) {
assert((width <= 256) && (height <= 256));
sendVRAMData(data, x, y, width, height);
waitForGPUDMADone();
GPU_GP0 = gp0_flushCache();
info->page = gp0_page(
x / 64,
y / 256,
GP0_BLEND_SEMITRANS,
GP0_COLOR_16BPP
);
info->clut = 0;
info->u = (uint8_t) (x % 64);
info->v = (uint8_t) (y % 256);
info->width = (uint16_t) width;
info->height = (uint16_t) height;
}
void uploadIndexedTexture(
TextureInfo *info,
const void *image,
const void *palette,
int imageX,
int imageY,
int paletteX,
int paletteY,
int width,
int height,
GP0ColorDepth colorDepth
) {
assert((width <= 256) && (height <= 256));
int numColors = (colorDepth == GP0_COLOR_8BPP) ? 256 : 16;
int widthDivider = (colorDepth == GP0_COLOR_8BPP) ? 2 : 4;
assert(!(paletteX % 16) && ((paletteX + numColors) <= 1024));
sendVRAMData(image, imageX, imageY, width / widthDivider, height);
waitForGPUDMADone();
sendVRAMData(palette, paletteX, paletteY, numColors, 1);
waitForGPUDMADone();
GPU_GP0 = gp0_flushCache();
info->page = gp0_page(
imageX / 64,
imageY / 256,
GP0_BLEND_SEMITRANS,
colorDepth
);
info->clut = gp0_clut(paletteX / 16, paletteY);
info->u = (uint8_t) ((imageX % 64) * widthDivider);
info->v = (uint8_t) (imageY % 256);
info->width = (uint16_t) width;
info->height = (uint16_t) height;
}
================================================
FILE: src/08_spinningCube/gpu.h
================================================
/*
* ps1-bare-metal - (C) 2023-2025 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
#pragma once
#include <stdint.h>
#include "ps1/gpucmd.h"
// In order for Z averaging to work properly, ORDERING_TABLE_SIZE should be set
// to either a relatively high value (1024 or more) or a multiple of 12; see
// setupGTE() for more details. Higher values will take up more memory but are
// required to render more complex scenes with wide depth ranges correctly.
#define GPU_CHAIN_BUFFER_SIZE 1024
#define GPU_ORDERING_TABLE_SIZE 240
typedef struct {
uint32_t data[GPU_CHAIN_BUFFER_SIZE];
uint32_t orderingTable[GPU_ORDERING_TABLE_SIZE];
uint32_t *nextPacket;
} GPUDMAChain;
typedef struct {
uint8_t u, v;
uint16_t width, height;
uint16_t page, clut;
} TextureInfo;
#ifdef __cplusplus
extern "C" {
#endif
void setupGPU(GP1VideoMode mode, int width, int height);
void waitForGP0Ready(void);
void waitForGPUDMADone(void);
void waitForVSync(void);
void sendGPULinkedList(const void *data);
void sendVRAMData(
const void *data,
int x,
int y,
int width,
int height
);
void clearOrderingTable(uint32_t *table, int numEntries);
uint32_t *allocateGP0Packet(GPUDMAChain *chain, int zIndex, int numCommands);
void uploadTexture(
TextureInfo *info,
const void *data,
int x,
int y,
int width,
int height
);
void uploadIndexedTexture(
TextureInfo *info,
const void *image,
const void *palette,
int imageX,
int imageY,
int paletteX,
int paletteY,
int width,
int height,
GP0ColorDepth colorDepth
);
#ifdef __cplusplus
}
#endif
================================================
FILE: src/08_spinningCube/main.c
================================================
/*
* ps1-bare-metal - (C) 2023-2025 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
/*
* Having explored the capabilities of the PS1's GPU in previous examples, it is
* now time to focus on the other piece of hardware that makes 3D graphics on
* the PS1 possible: the geometry transformation engine (GTE), a specialized
* coprocessor whose job is to perform various geometry-related calculations
* much faster than the CPU could on its own. To draw a 3D scene the CPU can use
* the GTE to calculate the screen space coordinates of each polygon's vertices,
* then pack those into a display list which will be sent off to the GPU for
* drawing. In this example we're going to draw a spinning model of a cube,
* using the GTE to carry out the computationally heavy tasks of rotation and
* perspective projection.
*
* Unlike any other peripheral on the console, the GTE is not memory-mapped
* but rather accessed through special CPU instructions that require the use of
* inline assembly. This tutorial will thus use the cop0.h and gte.h headers I
* wrote to abstract away the low-level assembly required to access GTE
* registers, focusing on its practical usage instead. This example may be
* harder to follow compared to previous ones for people unfamiliar with basic
* linear algebra and 3D geometry concepts, so familiarizing with those is
* highly recommended.
*/
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include "gpu.h"
#include "ps1/cop0.h"
#include "ps1/gpucmd.h"
#include "ps1/gte.h"
#include "ps1/registers.h"
#include "trig.h"
// The GTE uses a 20.12 fixed-point format for most values. What this means is
// that fractional values will be stored as integers by multiplying them by a
// fixed unit, in this case 4096 or 1 << 12 (hence making the fractional part 12
// bits long). We'll define this unit value to make their handling easier.
#define GTE_UNIT (1 << 12)
static void setupGTE(int width, int height) {
// Ensure the GTE, which is coprocessor 2, is enabled. MIPS coprocessors are
// enabled through the status register in coprocessor 0, which is always
// accessible.
cop0_setReg(COP0_STATUS, cop0_getReg(COP0_STATUS) | COP0_STATUS_CU2);
// Set the offset to be added to all calculated screen space coordinates (we
// want our cube to appear at the center of the screen) Note that OFX and
// OFY are 16.16 fixed-point rather than 20.12.
gte_setControlReg(GTE_OFX, (width << 16) / 2);
gte_setControlReg(GTE_OFY, (height << 16) / 2);
// Set the distance of the perspective projection plane (i.e. the camera's
// focal length), which affects the field of view.
int focalLength = (width < height) ? width : height;
gte_setControlReg(GTE_H, focalLength / 2);
// Set the scaling factor for Z averaging. For each polygon drawn, the GTE
// will sum the transformed Z coordinates of its vertices multiplied by this
// value in order to derive the ordering table bucket index the polygon will
// be sorted into. This will work best if the ordering table length is a
// multiple of 12 (i.e. both 3 and 4) or high enough to make any rounding
// error negligible.
gte_setControlReg(GTE_ZSF3, GPU_ORDERING_TABLE_SIZE / 3);
gte_setControlReg(GTE_ZSF4, GPU_ORDERING_TABLE_SIZE / 4);
}
// When transforming vertices, the GTE will multiply their vectors by a 3x3
// matrix stored in its registers. This matrix can be used, among other things,
// to rotate the model by multiplying it by the appropriate rotation matrices.
// The two functions below handle manipulation of this matrix.
static void multiplyCurrentMatrixByVectors(GTEMatrix *output) {
// Multiply the GTE's current matrix by the matrix whose column vectors are
// V0/V1/V2, then store the result to the provided location. This has to be
// done one column at a time, as the GTE only supports multiplying a matrix
// by a vector using the MVMVA command.
gte_command(GTE_CMD_MVMVA | GTE_SF | GTE_MX_RT | GTE_V_V0 | GTE_CV_NONE);
output->values[0][0] = (int16_t) gte_getDataReg(GTE_IR1);
output->values[1][0] = (int16_t) gte_getDataReg(GTE_IR2);
output->values[2][0] = (int16_t) gte_getDataReg(GTE_IR3);
gte_command(GTE_CMD_MVMVA | GTE_SF | GTE_MX_RT | GTE_V_V1 | GTE_CV_NONE);
output->values[0][1] = (int16_t) gte_getDataReg(GTE_IR1);
output->values[1][1] = (int16_t) gte_getDataReg(GTE_IR2);
output->values[2][1] = (int16_t) gte_getDataReg(GTE_IR3);
gte_command(GTE_CMD_MVMVA | GTE_SF | GTE_MX_RT | GTE_V_V2 | GTE_CV_NONE);
output->values[0][2] = (int16_t) gte_getDataReg(GTE_IR1);
output->values[1][2] = (int16_t) gte_getDataReg(GTE_IR2);
output->values[2][2] = (int16_t) gte_getDataReg(GTE_IR3);
}
static void rotateCurrentMatrix(int yaw, int pitch, int roll) {
static GTEMatrix multiplied;
int s, c;
// For each axis, compute the rotation matrix then "combine" it with the
// GTE's current matrix by multiplying the two and writing the result back
// to the GTE's registers.
if (yaw) {
s = isin(yaw);
c = icos(yaw);
gte_setColumnVectors(
c, -s, 0,
s, c, 0,
0, 0, GTE_UNIT
);
multiplyCurrentMatrixByVectors(&multiplied);
gte_loadRotationMatrix(&multiplied);
}
if (pitch) {
s = isin(pitch);
c = icos(pitch);
gte_setColumnVectors(
c, 0, s,
0, GTE_UNIT, 0,
-s, 0, c
);
multiplyCurrentMatrixByVectors(&multiplied);
gte_loadRotationMatrix(&multiplied);
}
if (roll) {
s = isin(roll);
c = icos(roll);
gte_setColumnVectors(
GTE_UNIT, 0, 0,
0, c, -s,
0, s, c
);
multiplyCurrentMatrixByVectors(&multiplied);
gte_loadRotationMatrix(&multiplied);
}
}
// We're going to store the 3D model of our cube as two separate arrays, one
// containing a list of unique vertices and the other referencing those vertices
// to build up quadrilateral faces. This approach of having a "palette" of
// vertices, in a similar way to how indexed color works, allows for significant
// memory savings as most if not all faces usually have vertices in common.
typedef struct {
uint8_t vertices[4];
uint32_t color;
} Face;
#define NUM_CUBE_VERTICES 8
#define NUM_CUBE_FACES 6
static const GTEVector16 cubeVertices[NUM_CUBE_VERTICES] = {
{ .x = -32, .y = -32, .z = -32 },
{ .x = 32, .y = -32, .z = -32 },
{ .x = -32, .y = 32, .z = -32 },
{ .x = 32, .y = 32, .z = -32 },
{ .x = -32, .y = -32, .z = 32 },
{ .x = 32, .y = -32, .z = 32 },
{ .x = -32, .y = 32, .z = 32 },
{ .x = 32, .y = 32, .z = 32 }
};
// Note that there are several requirements on the order of vertices:
// - they must be arranged in a Z-like shape rather than clockwise or
// counterclockwise, since the GPU processes a quad with vertices (A, B, C, D)
// as two triangles with vertices (A, B, C) and (B, C, D) respectively;
// - the first 3 vertices must be ordered clockwise when the face is viewed from
// the front, as the code relies on this to determine whether or not the quad
// is facing the camera (see main()).
// For instance, only the first of these faces (viewed from the front) has its
// vertices ordered correctly:
// 0----1 0----1 2----3
// | / | | \/ | | \ |
// | / | | /\ | | \ |
// 2----3 3----2 0----1
// Correct Not Z-shaped Not clockwise
static const Face cubeFaces[NUM_CUBE_FACES] = {
{ .vertices = { 0, 1, 2, 3 }, .color = 0x0000ff },
{ .vertices = { 6, 7, 4, 5 }, .color = 0x00ff00 },
{ .vertices = { 4, 5, 0, 1 }, .color = 0x00ffff },
{ .vertices = { 7, 6, 3, 2 }, .color = 0xff0000 },
{ .vertices = { 6, 4, 2, 0 }, .color = 0xff00ff },
{ .vertices = { 5, 7, 1, 3 }, .color = 0xffff00 }
};
#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 240
int main(int argc, const char **argv) {
initSerialIO(115200);
if ((GPU_GP1 & GP1_STAT_FB_MODE_BITMASK) == GP1_STAT_FB_MODE_PAL) {
puts("Using PAL mode");
setupGPU(GP1_MODE_PAL, SCREEN_WIDTH, SCREEN_HEIGHT);
} else {
puts("Using NTSC mode");
setupGPU(GP1_MODE_NTSC, SCREEN_WIDTH, SCREEN_HEIGHT);
}
setupGTE(SCREEN_WIDTH, SCREEN_HEIGHT);
GPUDMAChain dmaChains[2];
bool usingSecondFrame = false;
int frameCounter = 0;
for (;;) {
int bufferX = usingSecondFrame ? SCREEN_WIDTH : 0;
int bufferY = 0;
GPUDMAChain *chain = &dmaChains[usingSecondFrame];
usingSecondFrame = !usingSecondFrame;
uint32_t *ptr;
GPU_GP1 = gp1_fbOffset(bufferX, bufferY);
clearOrderingTable(chain->orderingTable, GPU_ORDERING_TABLE_SIZE);
chain->nextPacket = chain->data;
// Reset the GTE's translation vector (added to each vertex) and
// transformation matrix, then modify the matrix to rotate the cube. The
// translation vector is used here to move the cube away from the camera
// so it can be seen.
gte_setControlReg(GTE_TRX, 0);
gte_setControlReg(GTE_TRY, 0);
gte_setControlReg(GTE_TRZ, 128);
gte_setRotationMatrix(
GTE_UNIT, 0, 0,
0, GTE_UNIT, 0,
0, 0, GTE_UNIT
);
rotateCurrentMatrix(0, frameCounter * 16, frameCounter * 12);
frameCounter++;
// Draw the cube one face at a time.
for (int i = 0; i < NUM_CUBE_FACES; i++) {
const Face *face = &cubeFaces[i];
// Apply perspective projection to the first 3 vertices. The GTE can
// only process up to 3 vertices at a time, so we'll transform the
// last one separately.
gte_loadV0(&cubeVertices[face->vertices[0]]);
gte_loadV1(&cubeVertices[face->vertices[1]]);
gte_loadV2(&cubeVertices[face->vertices[2]]);
gte_command(GTE_CMD_RTPT | GTE_SF);
// Determine the winding order of the vertices on screen. If they
// are ordered clockwise then the face is visible, otherwise it can
// be culled as it is not facing the camera. Note that
// gte_getDataReg() always returns a 32-bit unsigned value, but most
// GTE registers should be interpreted as signed.
gte_command(GTE_CMD_NCLIP);
if (((int) gte_getDataReg(GTE_MAC0)) <= 0)
continue;
// Save the first transformed vertex (the GTE only keeps the X/Y
// coordinates of the last 3 vertices processed and Z coordinates of
// the last 4 vertices processed) and apply projection to the last
// vertex.
uint32_t xy0 = gte_getDataReg(GTE_SXY0);
gte_loadV0(&cubeVertices[face->vertices[3]]);
gte_command(GTE_CMD_RTPS | GTE_SF);
// Calculate the average Z coordinate of all vertices and use it to
// determine the ordering table bucket index for this face.
gte_command(GTE_CMD_AVSZ4 | GTE_SF);
int zIndex = (int) gte_getDataReg(GTE_OTZ);
if ((zIndex < 0) || (zIndex >= GPU_ORDERING_TABLE_SIZE))
continue;
// Create a new quad and give its vertices the X/Y coordinates
// calculated by the GTE.
ptr = allocateGP0Packet(chain, zIndex, 5);
ptr[0] = face->color | gp0_shadedQuad(false, false, false);
ptr[1] = xy0;
gte_storeDataReg(GTE_SXY0, 2 * 4, ptr);
gte_storeDataReg(GTE_SXY1, 3 * 4, ptr);
gte_storeDataReg(GTE_SXY2, 4 * 4, ptr);
}
ptr = allocateGP0Packet(chain, GPU_ORDERING_TABLE_SIZE - 1, 3);
ptr[0] = gp0_rgb(64, 64, 64) | gp0_vramFill();
ptr[1] = gp0_xy(bufferX, bufferY);
ptr[2] = gp0_xy(SCREEN_WIDTH, SCREEN_HEIGHT);
ptr = allocateGP0Packet(chain, GPU_ORDERING_TABLE_SIZE - 1, 4);
ptr[0] = gp0_setPage(0, true, false);
ptr[1] = gp0_fbOffset1(bufferX, bufferY);
ptr[2] = gp0_fbOffset2(
bufferX + SCREEN_WIDTH - 1,
bufferY + SCREEN_HEIGHT - 1
);
ptr[3] = gp0_fbOrigin(bufferX, bufferY);
waitForGP0Ready();
waitForVSync();
sendGPULinkedList(&(chain->orderingTable)[GPU_ORDERING_TABLE_SIZE - 1]);
}
return 0;
}
================================================
FILE: src/08_spinningCube/trig.c
================================================
/*
* ps1-bare-metal - (C) 2023-2025 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
/*
* This is a fast lookup-table-less implementation of fixed-point sine and
* cosine, based on the isin_S4 implementation from:
* https://www.coranac.com/2009/07/sines
*/
#include "trig.h"
#define A (1 << 12)
#define B 19900
#define C 3516
int isin(int x) {
int c = x << (30 - ISIN_SHIFT);
x -= 1 << ISIN_SHIFT;
x <<= 31 - ISIN_SHIFT;
x >>= 31 - ISIN_SHIFT;
x *= x;
x >>= 2 * ISIN_SHIFT - 14;
int y = B - (x * C >> 14);
y = A - (x * y >> 16);
return (c >= 0) ? y : (-y);
}
int isin2(int x) {
int c = x << (30 - ISIN2_SHIFT);
x -= 1 << ISIN2_SHIFT;
x <<= 31 - ISIN2_SHIFT;
x >>= 31 - ISIN2_SHIFT;
x *= x;
x >>= 2 * ISIN2_SHIFT - 14;
int y = B - (x * C >> 14);
y = A - (x * y >> 16);
return (c >= 0) ? y : (-y);
}
================================================
FILE: src/08_spinningCube/trig.h
================================================
/*
* ps1-bare-metal - (C) 2023-2025 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
#pragma once
#define ISIN_SHIFT 10
#define ISIN2_SHIFT 15
#define ISIN_PI (1 << (ISIN_SHIFT + 1))
#define ISIN2_PI (1 << (ISIN2_SHIFT + 1))
#ifdef __cplusplus
extern "C" {
#endif
int isin(int x);
int isin2(int x);
static inline int icos(int x) {
return isin(x + (1 << ISIN_SHIFT));
}
static inline int icos2(int x) {
return isin2(x + (1 << ISIN2_SHIFT));
}
#ifdef __cplusplus
}
#endif
================================================
FILE: src/09_controllers/font.c
================================================
/*
* ps1-bare-metal - (C) 2023-2025 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
#include <stdint.h>
#include "font.h"
#include "gpu.h"
#include "ps1/gpucmd.h"
static const SpriteInfo fontSprites[] = {
{ .x = 6, .y = 0, .width = 2, .height = 9 }, // !
{ .x = 12, .y = 0, .width = 4, .height = 9 }, // "
{ .x = 18, .y = 0, .width = 6, .height = 9 }, // #
{ .x = 24, .y = 0, .width = 6, .height = 9 }, // $
{ .x = 30, .y = 0, .width = 6, .height = 9 }, // %
{ .x = 36, .y = 0, .width = 6, .height = 9 }, // &
{ .x = 42, .y = 0, .width = 2, .height = 9 }, // '
{ .x = 48, .y = 0, .width = 3, .height = 9 }, // (
{ .x = 54, .y = 0, .width = 3, .height = 9 }, // )
{ .x = 60, .y = 0, .width = 4, .height = 9 }, // *
{ .x = 66, .y = 0, .width = 6, .height = 9 }, // +
{ .x = 72, .y = 0, .width = 3, .height = 9 }, // ,
{ .x = 78, .y = 0, .width = 6, .height = 9 }, // -
{ .x = 84, .y = 0, .width = 2, .height = 9 }, // .
{ .x = 90, .y = 0, .width = 6, .height = 9 }, // /
{ .x = 0, .y = 9, .width = 6, .height = 9 }, // 0
{ .x = 6, .y = 9, .width = 6, .height = 9 }, // 1
{ .x = 12, .y = 9, .width = 6, .height = 9 }, // 2
{ .x = 18, .y = 9, .width = 6, .height = 9 }, // 3
{ .x = 24, .y = 9, .width = 6, .height = 9 }, // 4
{ .x = 30, .y = 9, .width = 6, .height = 9 }, // 5
{ .x = 36, .y = 9, .width = 6, .height = 9 }, // 6
{ .x = 42, .y = 9, .width = 6, .height = 9 }, // 7
{ .x = 48, .y = 9, .width = 6, .height = 9 }, // 8
{ .x = 54, .y = 9, .width = 6, .height = 9 }, // 9
{ .x = 60, .y = 9, .width = 2, .height = 9 }, // :
{ .x = 66, .y = 9, .width = 3, .height = 9 }, // ;
{ .x = 72, .y = 9, .width = 6, .height = 9 }, // <
{ .x = 78, .y = 9, .width = 6, .height = 9 }, // =
{ .x = 84, .y = 9, .width = 6, .height = 9 }, // >
{ .x = 90, .y = 9, .width = 6, .height = 9 }, // ?
{ .x = 0, .y = 18, .width = 6, .height = 9 }, // @
{ .x = 6, .y = 18, .width = 6, .height = 9 }, // A
{ .x = 12, .y = 18, .width = 6, .height = 9 }, // B
{ .x = 18, .y = 18, .width = 6, .height = 9 }, // C
{ .x = 24, .y = 18, .width = 6, .height = 9 }, // D
{ .x = 30, .y = 18, .width = 6, .height = 9 }, // E
{ .x = 36, .y = 18, .width = 6, .height = 9 }, // F
{ .x = 42, .y = 18, .width = 6, .height = 9 }, // G
{ .x = 48, .y = 18, .width = 6, .height = 9 }, // H
{ .x = 54, .y = 18, .width = 4, .height = 9 }, // I
{ .x = 60, .y = 18, .width = 5, .height = 9 }, // J
{ .x = 66, .y = 18, .width = 6, .height = 9 }, // K
{ .x = 72, .y = 18, .width = 6, .height = 9 }, // L
{ .x = 78, .y = 18, .width = 6, .height = 9 }, // M
{ .x = 84, .y = 18, .width = 6, .height = 9 }, // N
{ .x = 90, .y = 18, .width = 6, .height = 9 }, // O
{ .x = 0, .y = 27, .width = 6, .height = 9 }, // P
{ .x = 6, .y = 27, .width = 6, .height = 9 }, // Q
{ .x = 12, .y = 27, .width = 6, .height = 9 }, // R
{ .x = 18, .y = 27, .width = 6, .height = 9 }, // S
{ .x = 24, .y = 27, .width = 6, .height = 9 }, // T
{ .x = 30, .y = 27, .width = 6, .height = 9 }, // U
{ .x = 36, .y = 27, .width = 6, .height = 9 }, // V
{ .x = 42, .y = 27, .width = 6, .height = 9 }, // W
{ .x = 48, .y = 27, .width = 6, .height = 9 }, // X
{ .x = 54, .y = 27, .width = 6, .height = 9 }, // Y
{ .x = 60, .y = 27, .width = 6, .height = 9 }, // Z
{ .x = 66, .y = 27, .width = 3, .height = 9 }, // [
{ .x = 72, .y = 27, .width = 6, .height = 9 }, // Backslash
{ .x = 78, .y = 27, .width = 3, .height = 9 }, // ]
{ .x = 84, .y = 27, .width = 4, .height = 9 }, // ^
{ .x = 90, .y = 27, .width = 6, .height = 9 }, // _
{ .x = 0, .y = 36, .width = 3, .height = 9 }, // `
{ .x = 6, .y = 36, .width = 6, .height = 9 }, // a
{ .x = 12, .y = 36, .width = 6, .height = 9 }, // b
{ .x = 18, .y = 36, .width = 6, .height = 9 }, // c
{ .x = 24, .y = 36, .width = 6, .height = 9 }, // d
{ .x = 30, .y = 36, .width = 6, .height = 9 }, // e
{ .x = 36, .y = 36, .width = 5, .height = 9 }, // f
{ .x = 42, .y = 36, .width = 6, .height = 9 }, // g
{ .x = 48, .y = 36, .width = 5, .height = 9 }, // h
{ .x = 54, .y = 36, .width = 2, .height = 9 }, // i
{ .x = 60, .y = 36, .width = 4, .height = 9 }, // j
{ .x = 66, .y = 36, .width = 5, .height = 9 }, // k
{ .x = 72, .y = 36, .width = 2, .height = 9 }, // l
{ .x = 78, .y = 36, .width = 6, .height = 9 }, // m
{ .x = 84, .y = 36, .width = 5, .height = 9 }, // n
{ .x = 90, .y = 36, .width = 6, .height = 9 }, // o
{ .x = 0, .y = 45, .width = 6, .height = 9 }, // p
{ .x = 6, .y = 45, .width = 6, .height = 9 }, // q
{ .x = 12, .y = 45, .width = 6, .height = 9 }, // r
{ .x = 18, .y = 45, .width = 6, .height = 9 }, // s
{ .x = 24, .y = 45, .width = 5, .height = 9 }, // t
{ .x = 30, .y = 45, .width = 5, .height = 9 }, // u
{ .x = 36, .y = 45, .width = 6, .height = 9 }, // v
{ .x = 42, .y = 45, .width = 6, .height = 9 }, // w
{ .x = 48, .y = 45, .width = 6, .height = 9 }, // x
{ .x = 54, .y = 45, .width = 6, .height = 9 }, // y
{ .x = 60, .y = 45, .width = 5, .height = 9 }, // z
{ .x = 66, .y = 45, .width = 4, .height = 9 }, // {
{ .x = 72, .y = 45, .width = 2, .height = 9 }, // |
{ .x = 78, .y = 45, .width = 4, .height = 9 }, // }
{ .x = 84, .y = 45, .width = 6, .height = 9 }, // ~
{ .x = 90, .y = 45, .width = 6, .height = 9 } // Invalid character
};
void printString(
GPUDMAChain *chain,
const TextureInfo *font,
int x,
int y,
const char *str
) {
int currentX = x, currentY = y;
uint32_t *ptr;
// Start by sending a texture page command to tell the GPU to use the font's
// spritesheet. The page setting persists when drawing rectangles, so
// sending it here just once is enough.
ptr = allocateGP0Packet(chain, 1);
ptr[0] = gp0_setPage(font->page, false, false);
// Iterate over every character in the string.
for (; *str; str++) {
char ch = *str;
// Check if the character is "special" and shall be handled without
// drawing any sprite, or if it's invalid and should be rendered as a
// box with a question mark (character code 127).
switch (ch) {
case '\t':
currentX += FONT_TAB_WIDTH - 1;
currentX -= currentX % FONT_TAB_WIDTH;
continue;
case '\n':
currentX = x;
currentY += FONT_LINE_HEIGHT;
continue;
case ' ':
currentX += FONT_SPACE_WIDTH;
continue;
case '\x80' ... '\xff':
ch = '\x7f';
break;
}
// If the character was not a tab, newline or space, fetch its
// respective entry from the sprite coordinate table.
const SpriteInfo *sprite = &fontSprites[ch - FONT_FIRST_TABLE_CHAR];
// Draw the character, summing the UV coordinates of the spritesheet in
// VRAM to those of the sprite itself within the sheet. Enable blending
// to make sure any semitransparent pixels in the font get rendered
// correctly.
ptr = allocateGP0Packet(chain, 4);
ptr[0] = gp0_rectangle(true, true, true);
ptr[1] = gp0_xy(currentX, currentY);
ptr[2] = gp0_uv(font->u + sprite->x, font->v + sprite->y, font->clut);
ptr[3] = gp0_xy(sprite->width, sprite->height);
// Move onto the next character.
currentX += sprite->width;
}
}
================================================
FILE: src/09_controllers/font.h
================================================
/*
* ps1-bare-metal - (C) 2023-2025 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
#pragma once
#include <stdint.h>
#include "gpu.h"
#define FONT_FIRST_TABLE_CHAR '!'
#define FONT_SPACE_WIDTH 4
#define FONT_TAB_WIDTH 32
#define FONT_LINE_HEIGHT 10
typedef struct {
uint8_t x, y, width, height;
} SpriteInfo;
#ifdef __cplusplus
extern "C" {
#endif
void printString(
GPUDMAChain *chain,
const TextureInfo *font,
int x,
int y,
const char *str
);
#ifdef __cplusplus
}
#endif
================================================
FILE: src/09_controllers/gpu.c
================================================
/*
* ps1-bare-metal - (C) 2023-2025 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include "gpu.h"
#include "ps1/gpucmd.h"
#include "ps1/registers.h"
#define DMA_MAX_CHUNK_SIZE 16
void setupGPU(GP1VideoMode mode, int width, int height) {
int x = 0x760;
int y = (mode == GP1_MODE_PAL) ? 0xa3 : 0x88;
GP1HorizontalRes horizontalRes = GP1_HRES_320;
GP1VerticalRes verticalRes = GP1_VRES_256;
int offsetX = (width * gp1_clockMultiplierH(horizontalRes)) / 2;
int offsetY = (height / gp1_clockDividerV(verticalRes)) / 2;
GPU_GP1 = gp1_resetGPU();
GPU_GP1 = gp1_fbRangeH(x - offsetX, x + offsetX);
GPU_GP1 = gp1_fbRangeV(y - offsetY, y + offsetY);
GPU_GP1 = gp1_fbMode(
horizontalRes,
verticalRes,
mode,
false,
GP1_COLOR_16BPP
);
GPU_GP1 = gp1_dispBlank(false);
DMA_DPCR |= DMA_DPCR_CH_ENABLE(DMA_GPU);
DMA_CHCR(DMA_GPU) = 0;
GPU_GP1 = gp1_dmaRequestMode(GP1_DREQ_GP0_WRITE);
}
void waitForGP0Ready(void) {
while (!(GPU_GP1 & GP1_STAT_CMD_READY))
__asm__ volatile("");
}
void waitForGPUDMADone(void) {
while (DMA_CHCR(DMA_GPU) & DMA_CHCR_ENABLE)
__asm__ volatile("");
}
void waitForVSync(void) {
while (!(IRQ_STAT & (1 << IRQ_VSYNC)))
__asm__ volatile("");
IRQ_STAT = ~(1 << IRQ_VSYNC);
}
void sendGPULinkedList(const void *data) {
waitForGPUDMADone();
assert(!((uint32_t) data % 4));
DMA_MADR(DMA_GPU) = (uint32_t) data;
DMA_CHCR(DMA_GPU) = 0
| DMA_CHCR_WRITE
| DMA_CHCR_MODE_LIST
| DMA_CHCR_ENABLE;
}
void sendVRAMData(
const void *data,
int x,
int y,
int width,
int height
) {
waitForGPUDMADone();
assert(!((uint32_t) data % 4));
size_t length = (width * height + 1) / 2;
size_t chunkSize, numChunks;
if (length < DMA_MAX_CHUNK_SIZE) {
chunkSize = length;
numChunks = 1;
} else {
chunkSize = DMA_MAX_CHUNK_SIZE;
numChunks = length / DMA_MAX_CHUNK_SIZE;
assert(!(length % DMA_MAX_CHUNK_SIZE));
}
waitForGP0Ready();
GPU_GP0 = gp0_vramWrite();
GPU_GP0 = gp0_xy(x, y);
GPU_GP0 = gp0_xy(width, height);
DMA_MADR(DMA_GPU) = (uint32_t) data;
DMA_BCR (DMA_GPU) = chunkSize | (numChunks << 16);
DMA_CHCR(DMA_GPU) = 0
| DMA_CHCR_WRITE
| DMA_CHCR_MODE_SLICE
| DMA_CHCR_ENABLE;
}
uint32_t *allocateGP0Packet(GPUDMAChain *chain, int numCommands) {
assert((numCommands >= 0) && (numCommands <= DMA_MAX_CHUNK_SIZE));
uint32_t *ptr = chain->nextPacket;
chain->nextPacket += numCommands + 1;
*ptr = gp0_tag(numCommands, chain->nextPacket);
assert(chain->nextPacket < &(chain->data)[GPU_CHAIN_BUFFER_SIZE]);
return &ptr[1];
}
void uploadTexture(
TextureInfo *info,
const void *data,
int x,
int y,
int width,
int height
) {
assert((width <= 256) && (height <= 256));
sendVRAMData(data, x, y, width, height);
waitForGPUDMADone();
GPU_GP0 = gp0_flushCache();
info->page = gp0_page(
x / 64,
y / 256,
GP0_BLEND_SEMITRANS,
GP0_COLOR_16BPP
);
info->clut = 0;
info->u = (uint8_t) (x % 64);
info->v = (uint8_t) (y % 256);
info->width = (uint16_t) width;
info->height = (uint16_t) height;
}
void uploadIndexedTexture(
TextureInfo *info,
const void *image,
const void *palette,
int imageX,
int imageY,
int paletteX,
int paletteY,
int width,
int height,
GP0ColorDepth colorDepth
) {
assert((width <= 256) && (height <= 256));
int numColors = (colorDepth == GP0_COLOR_8BPP) ? 256 : 16;
int widthDivider = (colorDepth == GP0_COLOR_8BPP) ? 2 : 4;
assert(!(paletteX % 16) && ((paletteX + numColors) <= 1024));
sendVRAMData(image, imageX, imageY, width / widthDivider, height);
waitForGPUDMADone();
sendVRAMData(palette, paletteX, paletteY, numColors, 1);
waitForGPUDMADone();
GPU_GP0 = gp0_flushCache();
info->page = gp0_page(
imageX / 64,
imageY / 256,
GP0_BLEND_SEMITRANS,
colorDepth
);
info->clut = gp0_clut(paletteX / 16, paletteY);
info->u = (uint8_t) ((imageX % 64) * widthDivider);
info->v = (uint8_t) (imageY % 256);
info->width = (uint16_t) width;
info->height = (uint16_t) height;
}
================================================
FILE: src/09_controllers/gpu.h
================================================
/*
* ps1-bare-metal - (C) 2023-2025 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
#pragma once
#include <stdint.h>
#include "ps1/gpucmd.h"
#define GPU_CHAIN_BUFFER_SIZE 1024
typedef struct {
uint32_t data[GPU_CHAIN_BUFFER_SIZE];
uint32_t *nextPacket;
} GPUDMAChain;
typedef struct {
uint8_t u, v;
uint16_t width, height;
uint16_t page, clut;
} TextureInfo;
#ifdef __cplusplus
extern "C" {
#endif
void setupGPU(GP1VideoMode mode, int width, int height);
void waitForGP0Ready(void);
void waitForGPUDMADone(void);
void waitForVSync(void);
void sendGPULinkedList(const void *data);
void sendVRAMData(
const void *data,
int x,
int y,
int width,
int height
);
uint32_t *allocateGP0Packet(GPUDMAChain *chain, int numCommands);
void uploadTexture(
TextureInfo *info,
const void *data,
int x,
int y,
int width,
int height
);
void uploadIndexedTexture(
TextureInfo *info,
const void *image,
const void *palette,
int imageX,
int imageY,
int paletteX,
int paletteY,
int width,
int height,
GP0ColorDepth colorDepth
);
#ifdef __cplusplus
}
#endif
================================================
FILE: src/09_controllers/main.c
================================================
/*
* ps1-bare-metal - (C) 2023-2025 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
/*
* This example implements a simple controller tester, showing how to send
* commands to and obtain data from connected controllers.
*
* Compared to other consoles where the state of all inputs is sometimes easily
* accessible through registers, the PS1 has its controllers and memory cards
* interfaced via a serial bus (similar to SPI). Both communicate using a simple
* packet-based protocol, listening for request packets sent by the console and
* replying with appropriate responses. Each packet consists of an address, a
* command and a series of parameters, while responses will typically contain
* information about the controller and the current state of its buttons in
* addition to any data returned by the command.
*
* Communication is done over the SIO0 serial interface, similar to the SIO1
* interface we used in the hello world example. All ports share the same bus,
* so an addressing system is used to specify which device shall respond to each
* packet. Understanding this example may require some basic familiarity with
* serial ports and their usage, although I tried to add as many comments as I
* could to explain what is going on under the hood.
*/
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include "font.h"
#include "gpu.h"
#include "ps1/gpucmd.h"
#include "ps1/registers.h"
static void delayMicroseconds(int time) {
// Calculate the approximate number of CPU cycles that need to be burned,
// assuming a 33.8688 MHz clock (1 us = 33.8688 = ~33.875 = 271 / 8 cycles).
// The loop consists of a branch and a decrement, thus each iteration will
// burn 2 cycles.
time = ((time * 271) + 4) / 8;
__asm__ volatile(
// The .set noreorder directive will prevent the assembler from trying
// to "hide" the branch instruction's delay slot by shuffling nearby
// instructions. .set push and .set pop are used to save and restore the
// assembler's settings respectively, ensuring the noreorder flag will
// not affect any other code.
".set push\n"
".set noreorder\n"
"bgtz %0, .\n"
"addiu %0, -2\n"
".set pop\n"
: "+r"(time)
);
}
static void initControllerBus(void) {
// Reset the serial interface, initialize it with the settings used by
// controllers and memory cards (250000bps, 8 data bits) and configure it to
// send a signal to the interrupt controller whenever the DSR input is
// pulsed (see below).
SIO_CTRL(0) = SIO_CTRL_RESET;
SIO_MODE(0) = 0
| SIO_MODE_BAUD_DIV1
| SIO_MODE_DATA_8;
SIO_BAUD(0) = F_CPU / 250000;
SIO_CTRL(0) = 0
| SIO_CTRL_TX_ENABLE
| SIO_CTRL_RX_ENABLE
| SIO_CTRL_DSR_IRQ_ENABLE;
}
static bool waitForAcknowledge(int timeout) {
// Controllers and memory cards will acknowledge bytes received by sending
// short pulses over the DSR line, which will be forwarded by the serial
// interface to the interrupt controller. This is not guaranteed to happen
// (it will not if e.g. no device is connected), so we have to implement a
// timeout to avoid waiting forever in such cases.
for (; timeout > 0; timeout -= 10) {
if (IRQ_STAT & (1 << IRQ_SIO0)) {
// Reset the interrupt controller and serial interface's flags to
// ensure the interrupt can be triggered again.
IRQ_STAT = ~(1 << IRQ_SIO0);
SIO_CTRL(0) |= SIO_CTRL_ACKNOWLEDGE;
return true;
}
delayMicroseconds(10);
}
return false;
}
// As the controller bus is shared with memory cards, an addressing mechanism is
// used to ensure packets are processed by a single device at a time. The first
// byte of each request packet is thus the "address" of the peripheral that
// shall respond to it.
typedef enum {
SIO0_ADDR_CONTROLLER = 0x01,
SIO0_ADDR_MEMORY_CARD = 0x81
} SIO0DeviceAddress;
// The address is followed by a command byte and any required parameters. The
// only command used in this example (and supported by all controllers) is
// SIO0_PAD_POLL, however some controllers additionally support a "configuration
// mode" which grants access to an extended command set.
typedef enum {
// Controller commands
SIO0_PAD_POLL = 'B', // Read controller state
SIO0_PAD_CONFIG_MODE = 'C', // Enter or exit configuration mode
// Configuration mode commands
SIO0_CFG_INIT_PRESSURE = '@', // Initialize DualShock 2 pressure sensors
SIO0_CFG_SET_ANALOG = 'D', // Set analog mode/LED state
SIO0_CFG_GET_ANALOG = 'E', // Get analog mode/LED state
SIO0_CFG_GET_ACT_INFO = 'F', // Get information about a feedback actuator
SIO0_CFG_GET_ACT_LIST = 'G', // Get list of all feedback actuators
SIO0_CFG_GET_ACT_STATE = 'H', // Get current state of feedback actuators
SIO0_CFG_GET_MODE = 'L', // Get list of all supported modes
SIO0_CFG_REQUEST_SETUP = 'M', // Configure poll request format
SIO0_CFG_RESPONSE_SETUP = 'O', // Configure poll response format
// Memory card commands
SIO0_CARD_READ = 'R', // Read 128-byte sector
SIO0_CARD_GET_SIZE = 'S', // Retrieve size information
SIO0_CARD_WRITE = 'W' // Write 128-byte sector
} SIO0_DeviceCommand;
#define DTR_DELAY 60
#define DSR_TIMEOUT 120
static void selectControllerPort(int port) {
// Set or clear the bit that controls which set of controller and memory
// card ports is going to have its DTR (port select) signal asserted. The
// actual serial bus is shared between all ports, however devices will not
// process packets if DTR is not asserted on the port they are plugged into.
if (port)
SIO_CTRL(0) |= SIO_CTRL_CS_PORT_2;
else
SIO_CTRL(0) &= ~SIO_CTRL_CS_PORT_2;
}
static uint8_t exchangeByte(uint8_t value) {
// Wait until the interface is ready to accept a byte to send, then wait for
// it to finish receiving the byte sent by the device.
while (!(SIO_STAT(0) & SIO_STAT_TX_NOT_FULL))
__asm__ volatile("");
SIO_DATA(0) = value;
while (!(SIO_STAT(0) & SIO_STAT_RX_NOT_EMPTY))
__asm__ volatile("");
return SIO_DATA(0);
}
static size_t exchangeSIO0Packet(
SIO0DeviceAddress address,
const uint8_t *request,
uint8_t *response,
size_t reqLength,
size_t maxRespLength
) {
// Reset the interrupt flag and assert the DTR signal to tell the controller
// or memory card that we're about to send a packet. Devices may take some
// time to prepare for incoming bytes so we need a small delay here.
IRQ_STAT = ~(1 << IRQ_SIO0);
SIO_CTRL(0) |= SIO_CTRL_DTR | SIO_CTRL_ACKNOWLEDGE;
delayMicroseconds(DTR_DELAY);
size_t respLength = 0;
// Send the address byte and wait for the device to respond with a pulse on
// the DSR line. If no response is received assume no device is connected,
// otherwise make sure the serial interface's data buffer is empty to
// prepare for the actual packet transfer.
SIO_DATA(0) = address;
if (waitForAcknowledge(DSR_TIMEOUT)) {
while (SIO_STAT(0) & SIO_STAT_RX_NOT_EMPTY)
SIO_DATA(0);
// Send and receive the packet simultaneously one byte at a time,
// padding it with zeroes if the packet we are receiving is longer than
// the data being sent.
while (respLength < maxRespLength) {
if (reqLength > 0) {
*(response++) = exchangeByte(*(request++));
reqLength--;
} else {
*(response++) = exchangeByte(0);
}
respLength++;
// The device will keep sending DSR pulses as long as there is more
// data to transfer. If no more pulses are received, terminate the
// transfer.
if (!waitForAcknowledge(DSR_TIMEOUT))
break;
}
}
// Release DSR, allowing the device to go idle.
delayMicroseconds(DTR_DELAY);
SIO_CTRL(0) &= ~SIO_CTRL_DTR;
return respLength;
}
// All packets sent by controllers in response to a poll command include a 4-bit
// device type identifier as well as a bitfield describing the state of up to 16
// buttons.
static const char *const controllerTypes[] = {
"Unknown", // ID 0x0
"Mouse", // ID 0x1
"neGcon", // ID 0x2
"Konami Justifier", // ID 0x3
"Digital controller", // ID 0x4
"Analog stick", // ID 0x5
"Guncon", // ID 0x6
"Analog controller", // ID 0x7
"Multitap", // ID 0x8
"Keyboard", // ID 0x9
"Unknown", // ID 0xa
"Unknown", // ID 0xb
"Unknown", // ID 0xc
"Unknown", // ID 0xd
"Jogcon", // ID 0xe
"Configuration mode" // ID 0xf
};
static const char *const buttonNames[] = {
"Select", // Bit 0
"L3", // Bit 1
"R3", // Bit 2
"Start", // Bit 3
"Up", // Bit 4
"Right", // Bit 5
"Down", // Bit 6
"Left", // Bit 7
"L2", // Bit 8
"R2", // Bit 9
"L1", // Bit 10
"R1", // Bit 11
"Triangle", // Bit 12
"Circle", // Bit 13
"X", // Bit 14
"Square" // Bit 15
};
static void printControllerInfo(int port, char *output) {
// Build the request packet.
uint8_t request[4], response[8];
char *ptr = output;
request[0] = SIO0_PAD_POLL; // Command
request[1] = 0x00; // Multitap address
request[2] = 0x00; // Actuator control 1
request[3] = 0x00; // Actuator control 2
// Send the request to the specified controller port and grab the response.
// Note that this is a relatively slow process and should be done only once
// per frame, unless higher polling rates are desired.
selectControllerPort(port);
size_t respLength = exchangeSIO0Packet(
SIO0_ADDR_CONTROLLER,
request,
response,
sizeof(request),
sizeof(response)
);
ptr += sprintf(ptr, "Port %d:\n", port + 1);
if (respLength < 4) {
// All controllers reply with at least 4 bytes of data.
ptr += sprintf(ptr, " No controller connected");
return;
}
// The first byte of the response contains the device type ID in the upper
// nibble, as well as the length of the packet's payload in 2-byte units in
// the lower nibble.
ptr += sprintf(
ptr,
" Controller type:\t%s\n"
" Buttons pressed:\t",
controllerTypes[response[0] >> 4]
);
// Bytes 2 and 3 hold a bitfield representing the state all buttons. As each
// bit is active low (i.e. a zero represents a button being pressed), the
// entire field must be inverted.
uint16_t buttons = (response[2] | (response[3] << 8)) ^ 0xffff;
for (int i = 0; i < 16; i++) {
if ((buttons >> i) & 1)
ptr += sprintf(ptr, "%s ", buttonNames[i]);
}
ptr += sprintf(ptr, "\n Response data:\t");
for (size_t i = 0; i < respLength; i++)
ptr += sprintf(ptr, "%02X ", response[i]);
}
#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 240
#define FONT_WIDTH 96
#define FONT_HEIGHT 56
#define FONT_COLOR_DEPTH GP0_COLOR_4BPP
extern const uint8_t fontTexture[], fontPalette[];
int main(int argc, const char **argv) {
initSerialIO(115200);
initControllerBus();
if ((GPU_GP1 & GP1_STAT_FB_MODE_BITMASK) == GP1_STAT_FB_MODE_PAL) {
puts("Using PAL mode");
setupGPU(GP1_MODE_PAL, SCREEN_WIDTH, SCREEN_HEIGHT);
} else {
puts("Using NTSC mode");
setupGPU(GP1_MODE_NTSC, SCREEN_WIDTH, SCREEN_HEIGHT);
}
TextureInfo font;
uploadIndexedTexture(
&font,
fontTexture,
fontPalette,
SCREEN_WIDTH * 2,
0,
SCREEN_WIDTH * 2,
FONT_HEIGHT,
FONT_WIDTH,
FONT_HEIGHT,
FONT_COLOR_DEPTH
);
GPUDMAChain dmaChains[2];
bool usingSecondFrame = false;
for (;;) {
int bufferX = usingSecondFrame ? SCREEN_WIDTH : 0;
int bufferY = 0;
GPUDMAChain *chain = &dmaChains[usingSecondFrame];
usingSecondFrame = !usingSecondFrame;
uint32_t *ptr;
GPU_GP1 = gp1_fbOffset(bufferX, bufferY);
chain->nextPacket = chain->data;
ptr = allocateGP0Packet(chain, 4);
ptr[0] = gp0_setPage(0, true, false);
ptr[1] = gp0_fbOffset1(bufferX, bufferY);
ptr[2] = gp0_fbOffset2(
bufferX + SCREEN_WIDTH - 1,
bufferY + SCREEN_HEIGHT - 1
);
ptr[3] = gp0_fbOrigin(bufferX, bufferY);
ptr = allocateGP0Packet(chain, 3);
ptr[0] = gp0_rgb(64, 64, 64) | gp0_vramFill();
ptr[1] = gp0_xy(bufferX, bufferY);
ptr[2] = gp0_xy(SCREEN_WIDTH, SCREEN_HEIGHT);
// Poll both controller ports once per frame. Memory cards are ignored
// in this example.
for (int i = 0; i < 2; i++) {
int offset = i * 64;
char buffer[256];
printControllerInfo(i, buffer);
printString(chain, &font, 16, 32 + offset, buffer);
}
*(chain->nextPacket) = gp0_endTag(0);
waitForGP0Ready();
waitForVSync();
sendGPULinkedList(chain->data);
}
return 0;
}
================================================
FILE: src/libc/assert.h
================================================
/*
* ps1-bare-metal - (C) 2023 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
#pragma once
// NDEBUG is automatically defined by CMake when the executable is built in
// release mode.
#ifdef NDEBUG
#define assert(expr) ((void) (expr))
#else
#define assert(expr) \
((expr) ? ((void) 0) : _assertAbort(__FILE__, __LINE__, #expr))
#endif
#ifdef __cplusplus
extern "C" {
#endif
void _assertAbort(const char *file, int line, const char *expr);
#ifdef __cplusplus
}
#endif
================================================
FILE: src/libc/clz.s
================================================
# ps1-bare-metal - (C) 2023-2025 spicyjpeg
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
.set noreorder
# libgcc provides two functions used internally by GCC to count the number of
# leading zeroes in a value, __clzsi2() (32-bit) and __clzdi2() (64-bit). We're
# going to override them with smaller implementations that make use of the GTE's
# LZCS/LZCR registers.
.set GTE_LZCS, $30 # Leading zero count input
.set GTE_LZCR, $31 # Leading zero count output
.section .text.__clzsi2, "ax", @progbits
.global __clzsi2
.type __clzsi2, @function
__clzsi2:
# if (value & (1 << 31))
# return 0;
mtc2 $a0, GTE_LZCS
bltz $a0, .Lreturn
li $v0, 0
# else
# return countLeadingZeroes(value);
mfc2 $v0, GTE_LZCR
.Lreturn:
jr $ra
nop
.section .text.__clzdi2, "ax", @progbits
.global __clzdi2
.type __clzdi2, @function
__clzdi2:
# if (msb & (1 << 31))
# return 0;
mtc2 $a1, GTE_LZCS
bltz $a1, .Lreturn2
li $v0, 0
# else if (msb)
# return countLeadingZeroes(msb);
bnez $a1, .LreturnMSB
nop
.LnoMSB:
# else if (lsb & (1 << 31))
# return 32;
mtc2 $a0, GTE_LZCS
bltz $a0, .Lreturn2
li $v0, 32
# else
# return 32 + countLeadingZeroes(lsb);
mfc2 $v0, GTE_LZCR
jr $ra
addiu $v0, 32
.LreturnMSB:
mfc2 $v0, GTE_LZCR
.Lreturn2:
jr $ra
nop
================================================
FILE: src/libc/crt0.c
================================================
/*
* ps1-bare-metal - (C) 2023 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
#include <stddef.h>
#include <stdint.h>
typedef void (*Function)(void);
/* Linker symbols */
// These are defined by the linker script. Note that these are not variables,
// they are virtual symbols whose location matches their value. The simplest way
// to turn them into pointers is to declare them as arrays.
extern char _bssStart[], _bssEnd[];
extern const Function _preinitArrayStart[], _preinitArrayEnd[];
extern const Function _initArrayStart[], _initArrayEnd[];
extern const Function _finiArrayStart[], _finiArrayEnd[];
/* Heap API (used by malloc) */
#define ALIGN(x, n) (((x) + ((n) - 1)) & ~((n) - 1))
static uintptr_t _heapEnd = (uintptr_t) _bssEnd;
static uintptr_t _heapLimit = 0x80200000; // TODO: add a way to change this
void *sbrk(ptrdiff_t incr) {
uintptr_t currentEnd = _heapEnd;
uintptr_t newEnd = ALIGN(currentEnd + incr, 8);
if (newEnd >= _heapLimit)
return 0;
_heapEnd = newEnd;
return (void *) currentEnd;
}
/* Program entry point */
int main(int argc, const char **argv);
int _start(int argc, const char **argv) {
// Set $gp to point to the middle of the .sdata/.sbss sections, ensuring
// variables placed in those sections can be quickly accessed. See the
// linker script for more details.
__asm__ volatile("la $gp, _gp\n");
// Set all uninitialized variables to zero by clearing the BSS section.
__builtin_memset(_bssStart, 0, _bssEnd - _bssStart);
// Invoke all global constructors if any, then main() and finally all global
// destructors.
for (const Function *ctor = _preinitArrayStart; ctor < _preinitArrayEnd; ctor++)
(*ctor)();
for (const Function *ctor = _initArrayStart; ctor < _initArrayEnd; ctor++)
(*ctor)();
int returnValue = main(argc, argv);
for (const Function *dtor = _finiArrayStart; dtor < _finiArrayEnd; dtor++)
(*dtor)();
return returnValue;
}
================================================
FILE: src/libc/ctype.h
================================================
/*
* ps1-bare-metal - (C) 2023 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
int isprint(int ch);
int isgraph(int ch);
int isspace(int ch);
int isblank(int ch);
int isalpha(int ch);
int isdigit(int ch);
int tolower(int ch);
int toupper(int ch);
#ifdef __cplusplus
}
#endif
================================================
FILE: src/libc/cxxsupport.cpp
================================================
/*
* ps1-bare-metal - (C) 2023 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
#include <stddef.h>
#include <stdlib.h>
/* Allocating new/delete operators */
extern "C" void *__builtin_new(size_t size) {
return malloc(size);
}
extern "C" void __builtin_delete(void *ptr) {
free(ptr);
}
void *operator new(size_t size) noexcept {
return malloc(size);
}
void *operator new[](size_t size) noexcept {
return malloc(size);
}
void operator delete(void *ptr) noexcept {
free(ptr);
}
void operator delete[](void *ptr) noexcept {
free(ptr);
}
void operator delete(void *ptr, size_t size) noexcept {
free(ptr);
}
void operator delete[](void *ptr, size_t size) noexcept {
free(ptr);
}
/* Placement new/delete operators */
void *operator new(size_t size, void *ptr) noexcept {
return ptr;
}
void *operator new[](size_t size, void *ptr) noexcept {
return ptr;
}
void operator delete(void *ptr, void *place) noexcept {}
void operator delete[](void *ptr, void *place) noexcept {}
================================================
FILE: src/libc/malloc.c
================================================
/*
* ps1-bare-metal - (C) 2023 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*
* This code is based on psyqo's malloc implementation, available here:
* https://github.com/grumpycoders/pcsx-redux/blob/main/src/mips/psyqo/src/alloc.c
*/
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#define _align(x, n) (((x) + ((n) - 1)) & ~((n) - 1))
#define _updateHeapUsage(incr)
/* Internal state */
typedef struct _Block {
struct _Block *prev, *next;
void *ptr;
size_t size;
} Block;
static void *_mallocStart;
static Block *_mallocHead, *_mallocTail;
/* Allocator implementation */
static Block *_findBlock(Block *head, size_t size) {
Block *prev = head;
for (; prev; prev = prev->next) {
if (prev->next) {
uintptr_t nextBot = (uintptr_t) prev->next;
nextBot -= (uintptr_t) prev->ptr + prev->size;
if (nextBot >= size)
return prev;
}
}
return prev;
}
void *malloc(size_t size) {
if (!size)
return 0;
size_t _size = _align(size + sizeof(Block), 8);
// Nothing's initialized yet? Let's just initialize the bottom of our heap,
// flag it as allocated.
if (!_mallocHead) {
if (!_mallocStart)
_mallocStart = sbrk(0);
Block *new = (Block *) sbrk(_size);
if (!new)
return 0;
void *ptr = (void *) &new[1];
new->ptr = ptr;
new->size = _size - sizeof(Block);
new->prev = 0;
new->next = 0;
_mallocHead = new;
_mallocTail = new;
_updateHeapUsage(size);
return ptr;
}
// We *may* have the bottom of our heap that has shifted, because of a free.
// So let's check first if we have free space there, because I'm nervous
// about having an incomplete data structure.
if (((uintptr_t) _mallocStart + _size) < ((uintptr_t) _mallocHead)) {
Block *new = (Block *) _mallocStart;
void *ptr = (void *) &new[1];
new->ptr = ptr;
new->size = _size - sizeof(Block);
new->prev = 0;
new->next = _mallocHead;
_mallocHead->prev = new;
_mallocHead = new;
_updateHeapUsage(size);
return ptr;
}
// No luck at the beginning of the heap, let's walk the heap to find a fit.
Block *prev = _findBlock(_mallocHead, _size);
if (prev) {
Block *new = (Block *) ((uintptr_t) prev->ptr + prev->size);
void *ptr = (void *)((uintptr_t) new + sizeof(Block));
new->ptr = ptr;
new->size = _size - sizeof(Block);
new->prev = prev;
new->next = prev->next;
(new->next)->prev = new;
prev->next = new;
_updateHeapUsage(size);
return ptr;
}
// Time to extend the size of the heap.
Block *new = (Block *) sbrk(_size);
if (!new)
return 0;
void *ptr = (void *) &new[1];
new->ptr = ptr;
new->size = _size - sizeof(Block);
new->prev = _mallocTail;
new->next = 0;
_mallocTail->next = new;
_mallocTail = new;
_updateHeapUsage(size);
return ptr;
}
void *calloc(size_t num, size_t size) {
return malloc(num * size);
}
void *realloc(void *ptr, size_t size) {
if (!size) {
free(ptr);
return 0;
}
if (!ptr)
return malloc(size);
size_t _size = _align(size + sizeof(Block), 8);
Block *prev = (Block *) ((uintptr_t) ptr - sizeof(Block));
// New memory block shorter?
if (prev->size >= _size) {
_updateHeapUsage(size - prev->size);
prev->size = _size;
if (!prev->next)
sbrk((ptr - sbrk(0)) + _size);
return ptr;
}
// New memory block larger; is it the last one?
if (!prev->next) {
void *new = sbrk(_size - prev->size);
if (!new)
return 0;
_updateHeapUsage(size - prev->size);
prev->size = _size;
return ptr;
}
// Do we have free memory after it?
if (((prev->next)->ptr - ptr) > _size) {
_updateHeapUsage(size - prev->size);
prev->size = _size;
return ptr;
}
// No luck.
void *new = malloc(size);
if (!new)
return 0;
__builtin_memcpy(new, ptr, prev->size);
free(ptr);
return new;
}
void free(void *ptr) {
if (!ptr || !_mallocHead)
return;
// First block; bumping head ahead.
if (ptr == _mallocHead->ptr) {
size_t size = _mallocHead->size;
size += (uintptr_t) _mallocHead->ptr - (uintptr_t) _mallocHead;
_mallocHead = _mallocHead->next;
if (_mallocHead) {
_mallocHead->prev = 0;
} else {
_mallocTail = 0;
sbrk(-size);
}
_updateHeapUsage(-(_mallocHead->size));
return;
}
// Finding the proper block
Block *cur = _mallocHead;
for (cur = _mallocHead; ptr != cur->ptr; cur = cur->next) {
if (!cur->next)
return;
}
if (cur->next) {
// In the middle, just unlink it
(cur->next)->prev = cur->prev;
} else {
// At the end, shrink heap
void *top = sbrk(0);
size_t size = (top - (cur->prev)->ptr) - (cur->prev)->size;
_mallocTail = cur->prev;
sbrk(-size);
}
_updateHeapUsage(-(cur->size));
(cur->prev)->next = cur->next;
}
================================================
FILE: src/libc/misc.c
================================================
/*
* ps1-bare-metal - (C) 2023 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
#include <stdio.h>
#include "ps1/registers.h"
/* Serial port stdin/stdout */
void initSerialIO(int baud) {
SIO_CTRL(1) = SIO_CTRL_RESET;
SIO_MODE(1) = 0
| SIO_MODE_BAUD_DIV1
| SIO_MODE_DATA_8
| SIO_MODE_STOP_1;
SIO_BAUD(1) = F_CPU / baud;
SIO_CTRL(1) = 0
| SIO_CTRL_TX_ENABLE
| SIO_CTRL_RX_ENABLE
| SIO_CTRL_RTS;
}
void _putchar(char ch) {
// The serial interface will buffer but not send any data if the CTS input
// is not asserted, so we are going to abort if CTS is not set to avoid
// waiting forever.
while (
(SIO_STAT(1) & (SIO_STAT_TX_NOT_FULL | SIO_STAT_CTS)) == SIO_STAT_CTS
)
__asm__ volatile("");
if (SIO_STAT(1) & SIO_STAT_CTS)
SIO_DATA(1) = ch;
}
int _getchar(void) {
while (!(SIO_STAT(1) & SIO_STAT_RX_NOT_EMPTY))
__asm__ volatile("");
return SIO_DATA(1);
}
int _puts(const char *str) {
int length = 1;
for (; *str; str++, length++)
_putchar(*str);
_putchar('\n');
return length;
}
/* Abort functions */
void _assertAbort(const char *file, int line, const char *expr) {
#ifndef NDEBUG
printf("%s:%d: assert(%s)\n", file, line, expr);
#endif
for (;;)
__asm__ volatile("");
}
void abort(void) {
#ifndef NDEBUG
puts("abort()");
#endif
for (;;)
__asm__ volatile("");
}
void __cxa_pure_virtual(void) {
#ifndef NDEBUG
puts("__cxa_pure_virtual()");
#endif
for (;;)
__asm__ volatile("");
}
================================================
FILE: src/libc/setjmp.h
================================================
/*
* ps1-bare-metal - (C) 2023 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
#pragma once
#include <stdint.h>
typedef struct {
uint32_t ra;
uint32_t s0, s1, s2, s3, s4, s5, s6, s7;
uint32_t gp, sp, fp;
} jmp_buf;
#ifdef __cplusplus
extern "C" {
#endif
int setjmp(jmp_buf *env);
void longjmp(jmp_buf *env, int status);
#ifdef __cplusplus
}
#endif
================================================
FILE: src/libc/setjmp.s
================================================
# ps1-bare-metal - (C) 2023-2025 spicyjpeg
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
.set noreorder
# This is not a "proper" implementation of setjmp/longjmp as it does not save
# COP0 and GTE registers, but it is good enough for most use cases.
.section .text.setjmp, "ax", @progbits
.global setjmp
.type setjmp, @function
setjmp:
sw $ra, 0x00($a0)
sw $s0, 0x04($a0)
sw $s1, 0x08($a0)
sw $s2, 0x0c($a0)
sw $s3, 0x10($a0)
sw $s4, 0x14($a0)
sw $s5, 0x18($a0)
sw $s6, 0x1c($a0)
sw $s7, 0x20($a0)
sw $gp, 0x24($a0)
sw $sp, 0x28($a0)
sw $fp, 0x2c($a0)
# return 0;
jr $ra
li $v0, 0
.section .text.longjmp, "ax", @progbits
.global longjmp
.type longjmp, @function
longjmp:
lw $ra, 0x00($a0)
lw $s0, 0x04($a0)
lw $s1, 0x08($a0)
lw $s2, 0x0c($a0)
lw $s3, 0x10($a0)
lw $s4, 0x14($a0)
lw $s5, 0x18($a0)
lw $s6, 0x1c($a0)
lw $s7, 0x20($a0)
lw $gp, 0x24($a0)
lw $sp, 0x28($a0)
lw $fp, 0x2c($a0)
# return status;
jr $ra
move $v0, $a1
================================================
FILE: src/libc/stdio.h
================================================
/*
* ps1-bare-metal - (C) 2023 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
#pragma once
// Include printf() from the third-party library.
#include "vendor/printf.h"
#define putchar _putchar
#define getchar _getchar
#define puts _puts
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Initializes the serial port (SIO1) with the given baud rate, no
* parity, 8 data bits and 1 stop bit. Must be called prior to using putchar(),
* getchar(), puts() or printf().
*
* @param baud
*/
void initSerialIO(int baud);
void _putchar(char ch);
int _getchar(void);
int _puts(const char *str);
#ifdef __cplusplus
}
#endif
================================================
FILE: src/libc/stdlib.h
================================================
/*
* ps1-bare-metal - (C) 2023 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
#pragma once
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
static inline int abs(int value) {
return (value < 0) ? (-value) : value;
}
static inline long labs(long value) {
return (value < 0) ? (-value) : value;
}
void abort(void);
long strtol(const char *str, char **strEnd, int base);
long long strtoll(const char *str, char **strEnd, int base);
void *sbrk(ptrdiff_t incr);
void *malloc(size_t size);
void *calloc(size_t num, size_t size);
void *realloc(void *ptr, size_t size);
void free(void *ptr);
#ifdef __cplusplus
}
#endif
================================================
FILE: src/libc/string.c
================================================
/*
* ps1-bare-metal - (C) 2023 spicyjpeg
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
#include <ctype.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
/* Character manipulation */
int isprint(int ch) {
return (ch >= ' ') && (ch <= '~');
}
int isgraph(int ch) {
return (ch > ' ') && (ch <= '~');
}
int isspace(int ch) {
return (ch == ' ') || ((ch >= '\t') && (ch <= '\r'));
}
int isblank(int ch) {
return (ch == ' ') || (ch == '\t');
}
int isalpha(int ch) {
return ((ch >= 'A') && (ch <= 'Z')) || ((ch >= 'a') && (ch <= 'z'));
}
int isdigit(int ch) {
return (ch >= '0') && (ch <= '9');
}
int tolower(int ch) {
if ((ch >= 'A') && (ch <= 'Z'))
ch += 'a' - 'A';
return ch;
}
int toupper(int ch) {
if ((ch >= 'a') && (ch <= 'z'))
ch += 'A' - 'a';
return ch;
}
/* Memory buffer manipulation */
#if 0
void *memset(void *dest, int ch, size_t count) {
uint8_t *_dest = (uint8_t *) dest;
for (; count; count--)
*(_dest++) = (uint8_t) ch;
return dest;
}
void *memcpy(void *restrict dest, const void *restrict src, size_t count) {
uint8_t *_dest = (uint8_t *) dest;
const uint8_t *_src = (const uint8_t *) src;
for (; count; count--)
*(_dest++) = *(_src++);
return dest;
}
#endif
void *memccpy(void *restrict dest, const void *restrict src, int ch, size_t count) {
uint8_t *_dest = (uint8_t *) dest;
const uint8_t *_src = (const uint8_t *) src;
for (; count; count--) {
uint8_t a = *(_src++);
*(_dest++) = a;
if (a == ch)
return (void *) _dest;
}
return 0;
}
void *memmove(void *dest, const void *src, size_t count) {
uint8_t *_dest = (uint8_t *) dest;
const uint8_t *_src = (const uint8_t *) src;
if (_dest == _src)
return dest;
if ((_dest >= &_src[count]) || (&_dest[count] <= _src))
return memcpy(dest, src, count);
if (_dest < _src) { // Copy forwards
for (; count; count--)
*(_dest++) = *(_src++);
} else { // Copy backwards
_src += count;
_dest += count;
for (; count; count--)
*(--_dest) = *(--_src);
}
return dest;
}
int memcmp(const void *lhs, const void *rhs, size_t count) {
const uint8_t *_lhs = (const uint8_t *) lhs;
const uint8_t *_rhs = (const uint8_t *) rhs;
for (; count; count--) {
uint8_t a = *(_lhs++), b = *(_rhs++);
if (a != b)
return ((int) a) - ((int) b);
}
return 0;
}
void *memchr(const void *ptr, int ch, size_t count) {
const uint8_t *_ptr = (const uint8_t *) ptr;
for (; count; count--, _ptr++) {
if (*_ptr == ch)
return (void *) _ptr;
}
return 0;
}
/* String manipulation */
char *strcpy(char *restrict dest, const char *restrict src) {
char *_dest = dest;
while (*src)
*(_dest++) = *(src++);
*_dest = 0;
return dest;
}
char *strncpy(char *restrict dest, const char *restrict src, size_t count) {
char *_dest = dest;
for (; count && *src; count--)
*(_dest++) = *(src++);
for (; count; count--)
*(_dest++) = 0;
return dest;
}
int strcmp(const char *lhs, const char *rhs) {
for (;;) {
char a = *(lhs++), b = *(rhs++);
if (a != b)
return ((int) a) - ((int) b);
if (!a && !b)
return 0;
}
}
int strncmp(const char *lhs, const char *rhs, size_t count) {
for (; count && *lhs && *rhs; count--) {
char a = *(lhs++), b = *(rhs++);
if (a != b)
return ((int) a) - ((int) b);
}
return 0;
}
char *strchr(const char *str, int ch) {
for (; *str; str++) {
if (*str == ch)
return (char *) str;
}
return 0;
}
char *strrchr(const char *str, int ch) {
size_t length = strlen(str);
for (str += length; length; length--) {
str--;
if (*str == ch)
return (char *) str;
}
return 0;
}
char *strpbrk(const char *str, const char *breakSet) {
for (; *str; str++) {
char a = *str;
for (const char *ch = breakSet; *ch; ch++) {
if (a == *ch)
return (char *) str;
}
}
return 0;
}
char *strstr(const char *str, const char *substr) {
size_t length = strlen(substr);
if (!length)
return (char *) str;
for (; *str; str++) {
if (!memcmp(str, substr, length))
return (char *) str;
}
return 0;
}
size_t strlen(const char *str) {
size_t length = 0;
for (; *str; str++)
length++;
return length;
}
// Non-standard, used internally
size_t strnlen(const char *str, size_t count) {
size_t length = 0;
for (; *str && (length < count); str++)
length++;
return length;
}
char *strcat(char *restrict dest, const char *restrict src) {
char *_dest = &dest[strlen(dest)];
while (*src)
*(_dest++) = *(src++);
*_dest = 0;
return dest;
}
char *strncat(char *restrict dest, const char *restrict src, size_t count) {
char *_dest = &dest[strlen(dest)];
for (; count && *src; count--)
*(_dest++) = *(src++);
*_dest = 0;
return dest;
}
char *strdup(const char *str) {
size_t length = strlen(str) + 1;
char *copy = malloc(length);
if (!copy)
return 0;
memcpy(copy, str, length);
return copy;
}
char *strndup(const char *str, size_t count) {
size_t length = strnlen(str, count) + 1;
char *copy = malloc(length);
if (!copy)
return 0;
memcpy(copy, str, length);
return copy;
}
/* String tokenizer */
static char *_strtokPtr = 0, *_strtokEndPtr = 0;
char *strtok(char *restrict str, const char *restrict delim) {
if (str) {
_strtokPtr = str;
_strtokEndPtr = &str[strlen(str)];
}
if (_strtokPtr >= _strtokEndPtr)
return 0;
if (!(*_strtokPtr))
return 0;
char *split = strstr(_strtokPtr, delim);
char *token = _strtokPtr;
if (split) {
*(split++) = 0;
_strtokPtr = split;
} else {
_strtokPtr += strlen(token);
}
return token;
}
/* Number parsers */
long long strtoll(const char *restrict str, char **restrict strEnd, int base) {
if (!str)
return 0;
while (isspace(*str))
str++;
char sign = *str;
if ((sign == '+') || (sign == '-'))
str++;
while (isspace(*str))
str++;
// Parse any base prefix if present. If a base was specified make sure it
// matches, otherwise use it to determine which base the value is in.
long long value = 0;
if (*str == '0') {
int foundBase;
switch (str[1]) {
case 0:
goto _exit;
case 'X':
case 'x':
foundBase = 16;
str += 2;
break;
case 'O':
case 'o':
foundBase = 8;
str += 2;
break;
case 'B':
case 'b':
foundBase = 2;
str += 2;
break;
default:
// Numbers starting with a zero are *not* interpreted as octal
// unless base = 8.
foundBase = 0;
str++;
}
if (!base)
base = foundBase;
else if (foundBase && (base != foundBase))
return 0;
}
if (!base)
base = 10;
else if ((base < 2) || (base > 36))
gitextract_wxeqi4ti/
├── .clangd
├── .editorconfig
├── .github/
│ ├── scripts/
│ │ └── buildToolchain.sh
│ └── workflows/
│ └── build.yml
├── .gitignore
├── .markdownlint.json
├── CMakeLists.txt
├── CMakePresets.json
├── LICENSE
├── README.md
├── cmake/
│ ├── executable.ld
│ ├── setup.cmake
│ ├── toolchain.cmake
│ └── tools.cmake
├── src/
│ ├── 00_helloWorld/
│ │ └── main.c
│ ├── 01_basicGraphics/
│ │ └── main.c
│ ├── 02_doubleBuffer/
│ │ └── main.c
│ ├── 03_dmaChain/
│ │ └── main.c
│ ├── 04_textures/
│ │ └── main.c
│ ├── 05_palettes/
│ │ └── main.c
│ ├── 06_fonts/
│ │ ├── gpu.c
│ │ ├── gpu.h
│ │ └── main.c
│ ├── 07_orderingTable/
│ │ ├── gpu.c
│ │ ├── gpu.h
│ │ └── main.c
│ ├── 08_spinningCube/
│ │ ├── gpu.c
│ │ ├── gpu.h
│ │ ├── main.c
│ │ ├── trig.c
│ │ └── trig.h
│ ├── 09_controllers/
│ │ ├── font.c
│ │ ├── font.h
│ │ ├── gpu.c
│ │ ├── gpu.h
│ │ └── main.c
│ ├── libc/
│ │ ├── assert.h
│ │ ├── clz.s
│ │ ├── crt0.c
│ │ ├── ctype.h
│ │ ├── cxxsupport.cpp
│ │ ├── malloc.c
│ │ ├── misc.c
│ │ ├── setjmp.h
│ │ ├── setjmp.s
│ │ ├── stdio.h
│ │ ├── stdlib.h
│ │ ├── string.c
│ │ ├── string.h
│ │ └── string.s
│ ├── ps1/
│ │ ├── cache.h
│ │ ├── cache.s
│ │ ├── cdrom.h
│ │ ├── cop0.h
│ │ ├── gpucmd.h
│ │ ├── gte.h
│ │ └── registers.h
│ └── vendor/
│ ├── LICENSE.printf
│ ├── printf.c
│ └── printf.h
└── tools/
├── convertExecutable.py
├── convertImage.py
└── requirements.txt
SYMBOL INDEX (295 symbols across 37 files)
FILE: src/00_helloWorld/main.c
function printCharacter (line 33) | static void printCharacter(char ch) {
function main (line 49) | int main(int argc, const char **argv) {
FILE: src/01_basicGraphics/main.c
function setupGPU (line 43) | static void setupGPU(GP1VideoMode mode, int width, int height) {
function waitForGP0Ready (line 79) | static void waitForGP0Ready(void) {
function main (line 89) | int main(int argc, const char **argv) {
FILE: src/02_doubleBuffer/main.c
function setupGPU (line 47) | static void setupGPU(GP1VideoMode mode, int width, int height) {
function waitForGP0Ready (line 70) | static void waitForGP0Ready(void) {
function waitForVSync (line 75) | static void waitForVSync(void) {
function main (line 90) | int main(int argc, const char **argv) {
FILE: src/03_dmaChain/main.c
function setupGPU (line 42) | static void setupGPU(GP1VideoMode mode, int width, int height) {
function waitForGP0Ready (line 72) | static void waitForGP0Ready(void) {
function waitForVSync (line 77) | static void waitForVSync(void) {
function sendGPULinkedList (line 84) | static void sendGPULinkedList(const void *data) {
type GPUDMAChain (line 110) | typedef struct {
function main (line 143) | int main(int argc, const char **argv) {
FILE: src/04_textures/main.c
function setupGPU (line 44) | static void setupGPU(GP1VideoMode mode, int width, int height) {
function waitForGP0Ready (line 72) | static void waitForGP0Ready(void) {
function waitForGPUDMADone (line 77) | static void waitForGPUDMADone(void) {
function waitForVSync (line 82) | static void waitForVSync(void) {
function sendGPULinkedList (line 89) | static void sendGPULinkedList(const void *data) {
function sendVRAMData (line 102) | static void sendVRAMData(
type GPUDMAChain (line 152) | typedef struct {
type TextureInfo (line 171) | typedef struct {
function uploadTexture (line 177) | static void uploadTexture(
function main (line 224) | int main(int argc, const char **argv) {
FILE: src/05_palettes/main.c
function setupGPU (line 42) | static void setupGPU(GP1VideoMode mode, int width, int height) {
function waitForGP0Ready (line 70) | static void waitForGP0Ready(void) {
function waitForGPUDMADone (line 75) | static void waitForGPUDMADone(void) {
function waitForVSync (line 80) | static void waitForVSync(void) {
function sendGPULinkedList (line 87) | static void sendGPULinkedList(const void *data) {
function sendVRAMData (line 100) | static void sendVRAMData(
type GPUDMAChain (line 138) | typedef struct {
type TextureInfo (line 158) | typedef struct {
function uploadIndexedTexture (line 164) | static void uploadIndexedTexture(
function main (line 224) | int main(int argc, const char **argv) {
FILE: src/06_fonts/gpu.c
function setupGPU (line 26) | void setupGPU(GP1VideoMode mode, int width, int height) {
function waitForGP0Ready (line 54) | void waitForGP0Ready(void) {
function waitForGPUDMADone (line 59) | void waitForGPUDMADone(void) {
function waitForVSync (line 64) | void waitForVSync(void) {
function sendGPULinkedList (line 71) | void sendGPULinkedList(const void *data) {
function sendVRAMData (line 82) | void sendVRAMData(
function uploadTexture (line 130) | void uploadTexture(
function uploadIndexedTexture (line 157) | void uploadIndexedTexture(
FILE: src/06_fonts/gpu.h
type GPUDMAChain (line 24) | typedef struct {
type TextureInfo (line 29) | typedef struct {
FILE: src/06_fonts/main.c
type SpriteInfo (line 49) | typedef struct {
function printString (line 156) | static void printString(
function main (line 227) | int main(int argc, const char **argv) {
FILE: src/07_orderingTable/gpu.c
function setupGPU (line 26) | void setupGPU(GP1VideoMode mode, int width, int height) {
function waitForGP0Ready (line 58) | void waitForGP0Ready(void) {
function waitForGPUDMADone (line 63) | void waitForGPUDMADone(void) {
function waitForVSync (line 68) | void waitForVSync(void) {
function sendGPULinkedList (line 75) | void sendGPULinkedList(const void *data) {
function sendVRAMData (line 86) | void sendVRAMData(
function clearOrderingTable (line 122) | void clearOrderingTable(uint32_t *table, int numEntries) {
function uploadTexture (line 166) | void uploadTexture(
function uploadIndexedTexture (line 193) | void uploadIndexedTexture(
FILE: src/07_orderingTable/gpu.h
type GPUDMAChain (line 28) | typedef struct {
type TextureInfo (line 34) | typedef struct {
FILE: src/07_orderingTable/main.c
function main (line 46) | int main(int argc, const char **argv) {
FILE: src/08_spinningCube/gpu.c
function setupGPU (line 26) | void setupGPU(GP1VideoMode mode, int width, int height) {
function waitForGP0Ready (line 57) | void waitForGP0Ready(void) {
function waitForGPUDMADone (line 62) | void waitForGPUDMADone(void) {
function waitForVSync (line 67) | void waitForVSync(void) {
function sendGPULinkedList (line 74) | void sendGPULinkedList(const void *data) {
function sendVRAMData (line 85) | void sendVRAMData(
function clearOrderingTable (line 121) | void clearOrderingTable(uint32_t *table, int numEntries) {
function uploadTexture (line 150) | void uploadTexture(
function uploadIndexedTexture (line 177) | void uploadIndexedTexture(
FILE: src/08_spinningCube/gpu.h
type GPUDMAChain (line 29) | typedef struct {
type TextureInfo (line 35) | typedef struct {
FILE: src/08_spinningCube/main.c
function setupGTE (line 55) | static void setupGTE(int width, int height) {
function multiplyCurrentMatrixByVectors (line 87) | static void multiplyCurrentMatrixByVectors(GTEMatrix *output) {
function rotateCurrentMatrix (line 108) | static void rotateCurrentMatrix(int yaw, int pitch, int roll) {
type Face (line 158) | typedef struct {
function main (line 203) | int main(int argc, const char **argv) {
FILE: src/08_spinningCube/trig.c
function isin (line 29) | int isin(int x) {
function isin2 (line 44) | int isin2(int x) {
FILE: src/08_spinningCube/trig.h
function icos (line 31) | static inline int icos(int x) {
function icos2 (line 34) | static inline int icos2(int x) {
FILE: src/09_controllers/font.c
function printString (line 120) | void printString(
FILE: src/09_controllers/font.h
type SpriteInfo (line 27) | typedef struct {
FILE: src/09_controllers/gpu.c
function setupGPU (line 26) | void setupGPU(GP1VideoMode mode, int width, int height) {
function waitForGP0Ready (line 54) | void waitForGP0Ready(void) {
function waitForGPUDMADone (line 59) | void waitForGPUDMADone(void) {
function waitForVSync (line 64) | void waitForVSync(void) {
function sendGPULinkedList (line 71) | void sendGPULinkedList(const void *data) {
function sendVRAMData (line 82) | void sendVRAMData(
function uploadTexture (line 130) | void uploadTexture(
function uploadIndexedTexture (line 157) | void uploadIndexedTexture(
FILE: src/09_controllers/gpu.h
type GPUDMAChain (line 24) | typedef struct {
type TextureInfo (line 29) | typedef struct {
FILE: src/09_controllers/main.c
function delayMicroseconds (line 47) | static void delayMicroseconds(int time) {
function initControllerBus (line 69) | static void initControllerBus(void) {
function waitForAcknowledge (line 86) | static bool waitForAcknowledge(int timeout) {
type SIO0DeviceAddress (line 112) | typedef enum {
type SIO0_DeviceCommand (line 121) | typedef enum {
function selectControllerPort (line 146) | static void selectControllerPort(int port) {
function exchangeByte (line 157) | static uint8_t exchangeByte(uint8_t value) {
function exchangeSIO0Packet (line 171) | static size_t exchangeSIO0Packet(
function printControllerInfo (line 266) | static void printControllerInfo(int port, char *output) {
function main (line 330) | int main(int argc, const char **argv) {
FILE: src/libc/crt0.c
function _start (line 55) | int _start(int argc, const char **argv) {
FILE: src/libc/cxxsupport.cpp
function __builtin_delete (line 26) | void __builtin_delete(void *ptr) {
FILE: src/libc/malloc.c
type Block (line 29) | typedef struct _Block {
function Block (line 41) | static Block *_findBlock(Block *head, size_t size) {
function free (line 195) | void free(void *ptr) {
FILE: src/libc/misc.c
function initSerialIO (line 22) | void initSerialIO(int baud) {
function _putchar (line 36) | void _putchar(char ch) {
function _getchar (line 49) | int _getchar(void) {
function _puts (line 56) | int _puts(const char *str) {
function _assertAbort (line 68) | void _assertAbort(const char *file, int line, const char *expr) {
function abort (line 77) | void abort(void) {
function __cxa_pure_virtual (line 86) | void __cxa_pure_virtual(void) {
FILE: src/libc/setjmp.h
type jmp_buf (line 21) | typedef struct {
FILE: src/libc/stdlib.h
function abs (line 26) | static inline int abs(int value) {
function labs (line 29) | static inline long labs(long value) {
FILE: src/libc/string.c
function isprint (line 25) | int isprint(int ch) {
function isgraph (line 29) | int isgraph(int ch) {
function isspace (line 33) | int isspace(int ch) {
function isblank (line 37) | int isblank(int ch) {
function isalpha (line 41) | int isalpha(int ch) {
function isdigit (line 45) | int isdigit(int ch) {
function tolower (line 49) | int tolower(int ch) {
function toupper (line 56) | int toupper(int ch) {
function memcmp (line 124) | int memcmp(const void *lhs, const void *rhs, size_t count) {
function strcmp (line 172) | int strcmp(const char *lhs, const char *rhs) {
function strncmp (line 183) | int strncmp(const char *lhs, const char *rhs, size_t count) {
function strlen (line 242) | size_t strlen(const char *str) {
function strnlen (line 252) | size_t strnlen(const char *str, size_t count) {
function strtoll (line 333) | long long strtoll(const char *restrict str, char **restrict strEnd, int ...
function strtol (line 425) | long strtol(const char *restrict str, char **restrict strEnd, int base) {
FILE: src/ps1/cdrom.h
type CDROMXAHeader (line 25) | typedef struct __attribute__((packed)) {
type CDROMXASubmodeFlag (line 29) | typedef enum {
type CDROMXACodingInfoFlag (line 40) | typedef enum {
type CDROMMSF (line 53) | typedef struct __attribute__((packed)) {
type CDROMGetlocLResult (line 57) | typedef struct __attribute__((packed)) {
type CDROMGetlocPResult (line 63) | typedef struct __attribute__((packed)) {
type CDROMGetIDResult (line 68) | typedef struct __attribute__((packed)) {
type CDROMReportPacket (line 73) | typedef struct __attribute__((packed)) {
function cdrom_encodeBCD (line 79) | DEF(uint8_t) cdrom_encodeBCD(uint8_t value) {
function cdrom_decodeBCD (line 86) | DEF(uint8_t) cdrom_decodeBCD(uint8_t value) {
function cdrom_convertLBAToMSF (line 93) | DEF(void) cdrom_convertLBAToMSF(CDROMMSF *msf, unsigned int lba) {
function cdrom_convertMSFToLBA (line 101) | DEF(unsigned int) cdrom_convertMSFToLBA(const CDROMMSF *msf) {
type CDROMCommand (line 111) | typedef enum {
type CDROMTestCommand (line 150) | typedef enum {
type CDROMIRQType (line 168) | typedef enum {
type CDROMCommandStatusFlag (line 177) | typedef enum {
type CDROMCommandErrorFlag (line 188) | typedef enum {
type CDROMModeFlag (line 197) | typedef enum {
FILE: src/ps1/cop0.h
type COP0Register (line 25) | typedef enum {
type COP0DCICFlag (line 38) | typedef enum {
type COP0StatusFlag (line 56) | typedef enum {
type COP0CauseFlag (line 72) | typedef enum {
function cop0_setReg (line 94) | DEF(void) cop0_setReg(const COP0Register reg, uint32_t value) {
function cop0_getReg (line 97) | DEF(uint32_t) cop0_getReg(const COP0Register reg) {
function cop0_enableInterrupts (line 104) | DEF(void) cop0_enableInterrupts(void) {
function cop0_disableInterrupts (line 109) | DEF(uint32_t) cop0_disableInterrupts(void) {
FILE: src/ps1/gpucmd.h
function gp0_tag (line 27) | DEF(uint32_t) gp0_tag(size_t length, void *next) {
function gp0_endTag (line 33) | DEF(uint32_t) gp0_endTag(size_t length) {
type GP0BlendMode (line 39) | typedef enum {
type GP0ColorDepth (line 47) | typedef enum {
function gp0_page (line 54) | DEF(uint16_t) gp0_page(
function gp0_clut (line 67) | DEF(uint16_t) gp0_clut(unsigned int x, unsigned int y) {
function gp0_xy (line 73) | DEF(uint32_t) gp0_xy(int x, int y) {
function gp0_uv (line 78) | DEF(uint32_t) gp0_uv(unsigned int u, unsigned int v, uint16_t attr) {
function gp0_rgb (line 84) | DEF(uint32_t) gp0_rgb(uint8_t r, uint8_t g, uint8_t b) {
type GP0Command (line 93) | typedef enum {
type GP0MiscCommand (line 104) | typedef enum {
type GP0AttributeCommand (line 112) | typedef enum {
function _gp0_polygon (line 121) | DEF(uint32_t) _gp0_polygon(
function gp0_triangle (line 135) | DEF(uint32_t) gp0_triangle(bool textured, bool blend) {
function gp0_shadedTriangle (line 138) | DEF(uint32_t) gp0_shadedTriangle(bool gouraud, bool textured, bool blend) {
function gp0_quad (line 141) | DEF(uint32_t) gp0_quad(bool textured, bool blend) {
function gp0_shadedQuad (line 144) | DEF(uint32_t) gp0_shadedQuad(bool gouraud, bool textured, bool blend) {
function gp0_line (line 148) | DEF(uint32_t) gp0_line(bool gouraud, bool blend) {
function gp0_polyLine (line 153) | DEF(uint32_t) gp0_polyLine(bool gouraud, bool blend) {
function _gp0_rectangle (line 160) | DEF(uint32_t) _gp0_rectangle(
function gp0_rectangle (line 172) | DEF(uint32_t) gp0_rectangle(bool textured, bool unshaded, bool blend) {
function gp0_rectangle1x1 (line 175) | DEF(uint32_t) gp0_rectangle1x1(bool textured, bool unshaded, bool blend) {
function gp0_rectangle8x8 (line 178) | DEF(uint32_t) gp0_rectangle8x8(bool textured, bool unshaded, bool blend) {
function gp0_rectangle16x16 (line 181) | DEF(uint32_t) gp0_rectangle16x16(bool textured, bool unshaded, bool blen...
function gp0_vramBlit (line 185) | DEF(uint32_t) gp0_vramBlit(void) {
function gp0_vramWrite (line 188) | DEF(uint32_t) gp0_vramWrite(void) {
function gp0_vramRead (line 191) | DEF(uint32_t) gp0_vramRead(void) {
function gp0_flushCache (line 195) | DEF(uint32_t) gp0_flushCache(void) {
function gp0_vramFill (line 198) | DEF(uint32_t) gp0_vramFill(void) {
function gp0_irq (line 201) | DEF(uint32_t) gp0_irq(void) {
function gp0_setPage (line 205) | DEF(uint32_t) gp0_setPage(uint16_t page, bool dither, bool unlockFB) {
function gp0_setWindow (line 211) | DEF(uint32_t) gp0_setWindow(
function gp0_fbOffset1 (line 223) | DEF(uint32_t) gp0_fbOffset1(unsigned int x, unsigned int y) {
function gp0_fbOffset2 (line 228) | DEF(uint32_t) gp0_fbOffset2(unsigned int x, unsigned int y) {
function gp0_fbOrigin (line 233) | DEF(uint32_t) gp0_fbOrigin(int x, int y) {
function gp0_fbMask (line 238) | DEF(uint32_t) gp0_fbMask(bool setMask, bool useMask) {
type GP1HorizontalRes (line 246) | typedef enum {
type GP1VerticalRes (line 255) | typedef enum {
type GP1VideoMode (line 261) | typedef enum {
type GP1ColorDepth (line 267) | typedef enum {
type GP1DMARequestMode (line 273) | typedef enum {
type GP1VRAMSize (line 281) | typedef enum {
type GP1Command (line 287) | typedef enum {
function gp1_clockMultiplierH (line 301) | DEF(uint32_t) gp1_clockMultiplierH(GP1HorizontalRes horizontalRes) {
function gp1_clockDividerV (line 318) | DEF(uint32_t) gp1_clockDividerV(GP1VerticalRes verticalRes) {
function gp1_resetGPU (line 329) | DEF(uint32_t) gp1_resetGPU(void) {
function gp1_resetFIFO (line 332) | DEF(uint32_t) gp1_resetFIFO(void) {
function gp1_acknowledge (line 335) | DEF(uint32_t) gp1_acknowledge(void) {
function gp1_dispBlank (line 338) | DEF(uint32_t) gp1_dispBlank(bool blank) {
function gp1_dmaRequestMode (line 342) | DEF(uint32_t) gp1_dmaRequestMode(GP1DMARequestMode mode) {
function gp1_fbOffset (line 346) | DEF(uint32_t) gp1_fbOffset(unsigned int x, unsigned int y) {
function gp1_fbRangeH (line 351) | DEF(uint32_t) gp1_fbRangeH(unsigned int low, unsigned int high) {
function gp1_fbRangeV (line 356) | DEF(uint32_t) gp1_fbRangeV(unsigned int low, unsigned int high) {
function gp1_fbMode (line 361) | DEF(uint32_t) gp1_fbMode(
function gp1_vramSize (line 375) | DEF(uint32_t) gp1_vramSize(GP1VRAMSize size) {
FILE: src/ps1/gte.h
type GTEVector16 (line 29) | typedef struct __attribute__((aligned(4))) {
type GTEVector32 (line 34) | typedef struct __attribute__((aligned(4))) {
type GTEMatrix (line 38) | typedef struct __attribute__((aligned(4))) {
type GTECommandFlag (line 45) | typedef enum {
function gte_command (line 87) | DEF(void) gte_command(const uint32_t cmd) {
type GTEControlRegister (line 98) | typedef enum {
type GTEStatusFlag (line 133) | typedef enum {
function gte_setControlReg (line 159) | DEF(void) gte_setControlReg(const GTEControlRegister reg, uint32_t value) {
function gte_getControlReg (line 162) | DEF(uint32_t) gte_getControlReg(const GTEControlRegister reg) {
type GTEDataRegister (line 229) | typedef enum {
function gte_setDataReg (line 263) | DEF(void) gte_setDataReg(const GTEDataRegister reg, uint32_t value) {
function gte_getDataReg (line 266) | DEF(uint32_t) gte_getDataReg(const GTEDataRegister reg) {
function gte_loadDataReg (line 276) | DEF(void) gte_loadDataReg(
function gte_storeDataReg (line 283) | DEF(void) gte_storeDataReg(
function gte_setRowVectors (line 311) | DEF(void) gte_setRowVectors(
function gte_setColumnVectors (line 323) | DEF(void) gte_setColumnVectors(
FILE: src/ps1/registers.h
type BaseAddress (line 34) | typedef enum {
type BIUControlFlag (line 46) | typedef enum {
type SIOStatusFlag (line 76) | typedef enum {
type SIOModeFlag (line 89) | typedef enum {
type SIOControlFlag (line 110) | typedef enum {
type DRAMControlFlag (line 135) | typedef enum {
type IRQChannel (line 158) | typedef enum {
type DMAChannel (line 178) | typedef enum {
type DMACHCRFlag (line 188) | typedef enum {
type DMADICRFlag (line 208) | typedef enum {
type TimerControlFlag (line 230) | typedef enum {
type CDROMHSTSFlag (line 255) | typedef enum {
type CDROMHINTFlag (line 265) | typedef enum {
type CDROMHCHPCTLFlag (line 274) | typedef enum {
type CDROMHCLRCTLFlag (line 280) | typedef enum {
type CDROMCIFlag (line 292) | typedef enum {
type CDROMADPCTLFlag (line 299) | typedef enum {
type GP1StatusFlag (line 326) | typedef enum {
type MDECCommandFlag (line 378) | typedef enum {
type MDECStatusFlag (line 395) | typedef enum {
type MDECControlFlag (line 418) | typedef enum {
type SPUStatusFlag (line 429) | typedef enum {
type SPUControlFlag (line 447) | typedef enum {
type CPUBCCFlag (line 542) | typedef enum {
FILE: src/vendor/printf.c
type out_fct_wrap_type (line 124) | typedef struct {
function _out_buffer (line 131) | static inline void _out_buffer(char character, void* buffer, size_t idx,...
function _out_null (line 140) | static inline void _out_null(char character, void* buffer, size_t idx, s...
function _out_char (line 147) | static inline void _out_char(char character, void* buffer, size_t idx, s...
function _out_fct (line 157) | static inline void _out_fct(char character, void* buffer, size_t idx, si...
function _strnlen_s (line 169) | static inline unsigned int _strnlen_s(const char* str, size_t maxsize)
function _is_digit (line 179) | static inline bool _is_digit(char ch)
function _atoi (line 186) | static unsigned int _atoi(const char** str)
function _out_rev (line 197) | static size_t _out_rev(out_fct_type out, char* buffer, size_t idx, size_...
function _ntoa_format (line 225) | static size_t _ntoa_format(out_fct_type out, char* buffer, size_t idx, s...
function _ntoa_long (line 279) | static size_t _ntoa_long(out_fct_type out, char* buffer, size_t idx, siz...
function _ntoa_long_long (line 304) | static size_t _ntoa_long_long(out_fct_type out, char* buffer, size_t idx...
function _ftoa (line 337) | static size_t _ftoa(out_fct_type out, char* buffer, size_t idx, size_t m...
function _etoa (line 465) | static size_t _etoa(out_fct_type out, char* buffer, size_t idx, size_t m...
function _vsnprintf (line 575) | static int _vsnprintf(out_fct_type out, char* buffer, const size_t maxle...
function printf_ (line 860) | int printf_(const char* format, ...)
function sprintf_ (line 871) | int sprintf_(char* buffer, const char* format, ...)
function snprintf_ (line 881) | int snprintf_(char* buffer, size_t count, const char* format, ...)
function vprintf_ (line 891) | int vprintf_(const char* format, va_list va)
function vsnprintf_ (line 898) | int vsnprintf_(char* buffer, size_t count, const char* format, va_list va)
function fctprintf (line 904) | int fctprintf(void (*out)(char character, void* arg), void* arg, const c...
FILE: tools/convertExecutable.py
function alignToMultiple (line 24) | def alignToMultiple(data: bytearray, alignment: int):
function parseStructFromFile (line 30) | def parseStructFromFile(file: BinaryIO, _struct: Struct) -> tuple:
function parseStructsFromFile (line 33) | def parseStructsFromFile(
class ELFType (line 47) | class ELFType(IntEnum):
class ELFArchitecture (line 53) | class ELFArchitecture(IntEnum):
class ELFEndianness (line 56) | class ELFEndianness(IntEnum):
class ProgHeaderType (line 60) | class ProgHeaderType(IntEnum):
class ProgHeaderFlag (line 67) | class ProgHeaderFlag(IntFlag):
class Segment (line 73) | class Segment:
method isReadOnly (line 78) | def isReadOnly(self) -> bool:
class ELF (line 82) | class ELF:
method __init__ (line 83) | def __init__(self, file: BinaryIO):
method flatten (line 148) | def flatten(self, stripReadOnly: bool = False) -> tuple[int, bytearray]:
function createParser (line 175) | def createParser() -> ArgumentParser:
function main (line 233) | def main():
FILE: tools/convertImage.py
function quantizeImage (line 28) | def quantizeImage(imageObj: Image.Image, numColors: int) -> Image.Image:
function getImagePalette (line 56) | def getImagePalette(imageObj: Image.Image) -> NDArray[np.uint8]:
function to16bpp (line 81) | def to16bpp(
function convertIndexedImage (line 116) | def convertIndexedImage(
function createParser (line 156) | def createParser() -> ArgumentParser:
function main (line 218) | def main():
Condensed preview — 63 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (333K chars).
[
{
"path": ".clangd",
"chars": 478,
"preview": "# As clang/clangd's MIPS-I support is still experimental, some minor changes to\n# the GCC arguments it picks up from CMa"
},
{
"path": ".editorconfig",
"chars": 268,
"preview": "root = true\n\n[*]\nindent_style = tab\nindent_size = 4\ncharset = utf-8\nend_of_lin"
},
{
"path": ".github/scripts/buildToolchain.sh",
"chars": 2021,
"preview": "#!/bin/bash\n\nROOT_DIR=\"$(pwd)\"\nBINUTILS_VERSION=\"2.43\"\nGCC_VERSION=\"14.2.0\"\nNUM_JOBS=\"4\"\n\nif [ $# -eq 2 ]; then\n\tPACKAGE"
},
{
"path": ".github/workflows/build.yml",
"chars": 1486,
"preview": "# The GCC toolchain is stored in the GitHub Actions cache after being built. To\n# minimize build times, the toolchain bu"
},
{
"path": ".gitignore",
"chars": 290,
"preview": "# Do not include any hidden metadata saved by apps and the OS.\ndesktop.ini\n.DS_Store\n.vscode/\n\n# Do not include any buil"
},
{
"path": ".markdownlint.json",
"chars": 96,
"preview": "{\n\t\"line-length\": {\n\t\t\"tables\": false\n\t},\n\n\t\"emphasis-style\": false,\n\t\"no-inline-html\": false\n}\n"
},
{
"path": "CMakeLists.txt",
"chars": 4115,
"preview": "# ps1-bare-metal - (C) 2023-2025 spicyjpeg\n#\n# Permission to use, copy, modify, and/or distribute this software for any\n"
},
{
"path": "CMakePresets.json",
"chars": 1007,
"preview": "{\n\t\"version\": 6,\n\t\"cmakeMinimumRequired\": {\n\t\t\"major\": 3,\n\t\t\"minor\": 25,\n\t\t\"patch\": 0\n\t},\n\t\"configurePresets\": [\n\t\t{\n\t\t\t"
},
{
"path": "LICENSE",
"chars": 1066,
"preview": "MIT License\n\nCopyright (c) 2023 spicyjpeg\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\n"
},
{
"path": "README.md",
"chars": 14199,
"preview": "\n# PlayStation 1 bare-metal C examples\n\nThis repository contains a series of homebrew tutorials and well-commented\nexamp"
},
{
"path": "cmake/executable.ld",
"chars": 2905,
"preview": "/*\n * ps1-bare-metal - (C) 2023-2025 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software fo"
},
{
"path": "cmake/setup.cmake",
"chars": 4223,
"preview": "# ps1-bare-metal - (C) 2023-2024 spicyjpeg\n#\n# Permission to use, copy, modify, and/or distribute this software for any\n"
},
{
"path": "cmake/toolchain.cmake",
"chars": 4150,
"preview": "# ps1-bare-metal - (C) 2023-2024 spicyjpeg\n#\n# Permission to use, copy, modify, and/or distribute this software for any\n"
},
{
"path": "cmake/tools.cmake",
"chars": 4058,
"preview": "# ps1-bare-metal - (C) 2023-2025 spicyjpeg\n#\n# Permission to use, copy, modify, and/or distribute this software for any\n"
},
{
"path": "src/00_helloWorld/main.c",
"chars": 2841,
"preview": "/*\n * ps1-bare-metal - (C) 2023-2025 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software fo"
},
{
"path": "src/01_basicGraphics/main.c",
"chars": 5850,
"preview": "/*\n * ps1-bare-metal - (C) 2023-2025 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software fo"
},
{
"path": "src/02_doubleBuffer/main.c",
"chars": 5539,
"preview": "/*\n * ps1-bare-metal - (C) 2023-2025 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software fo"
},
{
"path": "src/03_dmaChain/main.c",
"chars": 8006,
"preview": "/*\n * ps1-bare-metal - (C) 2023-2025 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software fo"
},
{
"path": "src/04_textures/main.c",
"chars": 9666,
"preview": "/*\n * ps1-bare-metal - (C) 2023-2025 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software fo"
},
{
"path": "src/05_palettes/main.c",
"chars": 9203,
"preview": "/*\n * ps1-bare-metal - (C) 2023-2025 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software fo"
},
{
"path": "src/06_fonts/gpu.c",
"chars": 4890,
"preview": "/*\n * ps1-bare-metal - (C) 2023-2025 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software fo"
},
{
"path": "src/06_fonts/gpu.h",
"chars": 1900,
"preview": "/*\n * ps1-bare-metal - (C) 2023-2025 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software fo"
},
{
"path": "src/06_fonts/main.c",
"chars": 11568,
"preview": "/*\n * ps1-bare-metal - (C) 2023-2025 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software fo"
},
{
"path": "src/07_orderingTable/gpu.c",
"chars": 6499,
"preview": "/*\n * ps1-bare-metal - (C) 2023-2025 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software fo"
},
{
"path": "src/07_orderingTable/gpu.h",
"chars": 2230,
"preview": "/*\n * ps1-bare-metal - (C) 2023-2025 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software fo"
},
{
"path": "src/07_orderingTable/main.c",
"chars": 4699,
"preview": "/*\n * ps1-bare-metal - (C) 2023-2025 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software fo"
},
{
"path": "src/08_spinningCube/gpu.c",
"chars": 5457,
"preview": "/*\n * ps1-bare-metal - (C) 2023-2025 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software fo"
},
{
"path": "src/08_spinningCube/gpu.h",
"chars": 2371,
"preview": "/*\n * ps1-bare-metal - (C) 2023-2025 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software fo"
},
{
"path": "src/08_spinningCube/main.c",
"chars": 12307,
"preview": "/*\n * ps1-bare-metal - (C) 2023-2025 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software fo"
},
{
"path": "src/08_spinningCube/trig.c",
"chars": 1550,
"preview": "/*\n * ps1-bare-metal - (C) 2023-2025 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software fo"
},
{
"path": "src/08_spinningCube/trig.h",
"chars": 1185,
"preview": "/*\n * ps1-bare-metal - (C) 2023-2025 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software fo"
},
{
"path": "src/09_controllers/font.c",
"chars": 7797,
"preview": "/*\n * ps1-bare-metal - (C) 2023-2025 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software fo"
},
{
"path": "src/09_controllers/font.h",
"chars": 1244,
"preview": "/*\n * ps1-bare-metal - (C) 2023-2025 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software fo"
},
{
"path": "src/09_controllers/gpu.c",
"chars": 4891,
"preview": "/*\n * ps1-bare-metal - (C) 2023-2025 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software fo"
},
{
"path": "src/09_controllers/gpu.h",
"chars": 1900,
"preview": "/*\n * ps1-bare-metal - (C) 2023-2025 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software fo"
},
{
"path": "src/09_controllers/main.c",
"chars": 13159,
"preview": "/*\n * ps1-bare-metal - (C) 2023-2025 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software fo"
},
{
"path": "src/libc/assert.h",
"chars": 1173,
"preview": "/*\n * ps1-bare-metal - (C) 2023 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software for any"
},
{
"path": "src/libc/clz.s",
"chars": 1988,
"preview": "# ps1-bare-metal - (C) 2023-2025 spicyjpeg\n#\n# Permission to use, copy, modify, and/or distribute this software for any\n"
},
{
"path": "src/libc/crt0.c",
"chars": 2629,
"preview": "/*\n * ps1-bare-metal - (C) 2023 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software for any"
},
{
"path": "src/libc/ctype.h",
"chars": 1031,
"preview": "/*\n * ps1-bare-metal - (C) 2023 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software for any"
},
{
"path": "src/libc/cxxsupport.cpp",
"chars": 1688,
"preview": "/*\n * ps1-bare-metal - (C) 2023 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software for any"
},
{
"path": "src/libc/malloc.c",
"chars": 5380,
"preview": "/*\n * ps1-bare-metal - (C) 2023 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software for any"
},
{
"path": "src/libc/misc.c",
"chars": 2143,
"preview": "/*\n * ps1-bare-metal - (C) 2023 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software for any"
},
{
"path": "src/libc/setjmp.h",
"chars": 1056,
"preview": "/*\n * ps1-bare-metal - (C) 2023 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software for any"
},
{
"path": "src/libc/setjmp.s",
"chars": 1719,
"preview": "# ps1-bare-metal - (C) 2023-2025 spicyjpeg\n#\n# Permission to use, copy, modify, and/or distribute this software for any\n"
},
{
"path": "src/libc/stdio.h",
"chars": 1329,
"preview": "/*\n * ps1-bare-metal - (C) 2023 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software for any"
},
{
"path": "src/libc/stdlib.h",
"chars": 1353,
"preview": "/*\n * ps1-bare-metal - (C) 2023 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software for any"
},
{
"path": "src/libc/string.c",
"chars": 7880,
"preview": "/*\n * ps1-bare-metal - (C) 2023 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software for any"
},
{
"path": "src/libc/string.h",
"chars": 1874,
"preview": "/*\n * ps1-bare-metal - (C) 2023 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software for any"
},
{
"path": "src/libc/string.s",
"chars": 8177,
"preview": "# ps1-bare-metal - (C) 2023-2025 spicyjpeg\n#\n# Permission to use, copy, modify, and/or distribute this software for any\n"
},
{
"path": "src/ps1/cache.h",
"chars": 890,
"preview": "/*\n * ps1-bare-metal - (C) 2023-2025 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software fo"
},
{
"path": "src/ps1/cache.s",
"chars": 3595,
"preview": "# ps1-bare-metal - (C) 2023-2025 spicyjpeg\n#\n# Permission to use, copy, modify, and/or distribute this software for any\n"
},
{
"path": "src/ps1/cdrom.h",
"chars": 6899,
"preview": "/*\n * ps1-bare-metal - (C) 2023-2025 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software fo"
},
{
"path": "src/ps1/cop0.h",
"chars": 4895,
"preview": "/*\n * ps1-bare-metal - (C) 2023-2025 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software fo"
},
{
"path": "src/ps1/gpucmd.h",
"chars": 9517,
"preview": "/*\n * ps1-bare-metal - (C) 2023-2025 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software fo"
},
{
"path": "src/ps1/gte.h",
"chars": 11967,
"preview": "/*\n * ps1-bare-metal - (C) 2023-2025 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software fo"
},
{
"path": "src/ps1/registers.h",
"chars": 20791,
"preview": "/*\n * ps1-bare-metal - (C) 2023-2025 spicyjpeg\n *\n * Permission to use, copy, modify, and/or distribute this software fo"
},
{
"path": "src/vendor/LICENSE.printf",
"chars": 1080,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2014 Marco Paland\n\nPermission is hereby granted, free of charge, to any person obta"
},
{
"path": "src/vendor/printf.c",
"chars": 28002,
"preview": "///////////////////////////////////////////////////////////////////////////////\r\n// \\author (c) Marco Paland (info@palan"
},
{
"path": "src/vendor/printf.h",
"chars": 4982,
"preview": "///////////////////////////////////////////////////////////////////////////////\r\n// \\author (c) Marco Paland (info@palan"
},
{
"path": "tools/convertExecutable.py",
"chars": 6752,
"preview": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\n\"\"\"ELF executable to PlayStation 1 .EXE converter\n\nA very simple script "
},
{
"path": "tools/convertImage.py",
"chars": 6843,
"preview": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\n\"\"\"PlayStation 1 image/texture data converter\n\nA simple script to conver"
},
{
"path": "tools/requirements.txt",
"chars": 440,
"preview": "# Create a Python virtual environment and install the dependencies listed below\n# by running the following commands:\n#\n#"
}
]
About this extraction
This page contains the full source code of the spicyjpeg/ps1-bare-metal GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 63 files (303.9 KB), approximately 96.9k tokens, and a symbol index with 295 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.