diff --git a/.gitattributes b/.gitattributes
index e53d046df..e8a92bf9a 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -2,3 +2,4 @@
# These are all bash scripts and should use lf
*.sh text eol=lf
tools/ipl3.h linguist-generated=true
+libdragon.version export-subst
diff --git a/.github/actions/path-diff/action.yml b/.github/actions/path-diff/action.yml
index 2120b3d5a..aea55f42b 100644
--- a/.github/actions/path-diff/action.yml
+++ b/.github/actions/path-diff/action.yml
@@ -19,7 +19,7 @@ runs:
using: "composite"
steps:
- name: Checkout
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
with:
fetch-depth: 0
diff --git a/.github/workflows/build-documentation.yml b/.github/workflows/build-documentation.yml
index 42b384bed..ad638d840 100644
--- a/.github/workflows/build-documentation.yml
+++ b/.github/workflows/build-documentation.yml
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
with:
fetch-depth: 1 # Using a shallow checkout. Change to `0` if a full fetch is required.
@@ -24,7 +24,7 @@ jobs:
echo "default_ref=${{ format('refs/heads/{0}', github.event.repository.default_branch) }}" >> $GITHUB_OUTPUT
- name: Run Doxygen
- uses: mattnotmitt/doxygen-action@v1.9
+ uses: mattnotmitt/doxygen-action@v1.12
with:
doxyfile-path: 'doxygen-public.conf'
diff --git a/.github/workflows/build-tool-windows.yml b/.github/workflows/build-tool-windows.yml
index 4b4e3dd15..284722863 100644
--- a/.github/workflows/build-tool-windows.yml
+++ b/.github/workflows/build-tool-windows.yml
@@ -22,7 +22,7 @@ jobs:
unzip
update: true
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
with:
fetch-depth: 1 # Using a shallow checkout. Change to `0` if a full fetch is required.
diff --git a/.github/workflows/build-toolchain-library-and-roms.yml b/.github/workflows/build-toolchain-library-and-roms.yml
index 4538503c9..c01d902bf 100644
--- a/.github/workflows/build-toolchain-library-and-roms.yml
+++ b/.github/workflows/build-toolchain-library-and-roms.yml
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
with:
fetch-depth: 0 # Using a full fetch so that the diff action can run.
@@ -101,16 +101,39 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max
+ # Cache the videoplayer sample movie so CI does not re-download it from
+ # archive.org on every run. The Makefile skips wget/curl if the file exists,
+ # and the Docker build binds the workspace, so restoring here is enough.
+ - name: Cache videoplayer sample movie
+ uses: actions/cache@v6
+ with:
+ path: examples/videoplayer/caminandes.ogv
+ key: videoplayer-caminandes-ogv-v1
+
# As we have a tagged image now, we can use that to run build.sh if it is
# built in the previous step. o/w it will be downloaded from the registry.
# Then verify everything is building properly
- - name: Build libdragon
+ - name: Run libdragon docker container
run: |
- docker run \
+ docker run -d --name libdragon \
--mount type=bind,source=$(pwd),target=/libdragon \
--workdir=/libdragon \
ghcr.io/${{ steps.vars.outputs.repository_name }}:latest \
- ./build.sh
+ sleep infinity
+
+ - name: Build libdragon
+ run: |
+ docker container exec --env=LIBDRAGON_PREVIEW=0 libdragon ./build.sh --no-examples
+
+ # This also builds the test ROMs, and checks that the preview APIs are
+ # locked for a stable project and unlocked for a preview one.
+ - name: Run tests
+ run: |
+ docker container exec --env=LIBDRAGON_PREVIEW=0 libdragon make test
+
+ - name: Build examples
+ run: |
+ docker container exec --env=LIBDRAGON_PREVIEW=0 libdragon make examples --keep-going
- name: "Upload built ROMs to artifacts"
uses: actions/upload-artifact@v7
@@ -120,6 +143,10 @@ jobs:
${{ github.workspace }}/examples/**/*.z64
${{ github.workspace }}/tests/*.z64
+ - name: Stop libdragon docker container
+ if: always()
+ run: docker rm -f libdragon
+
# Finally push the verified image to the registry with the latest tag if
# we are on the default branch. At this point, we know that libdragon can
# build with this freshly built image.
diff --git a/.github/workflows/build-toolchain-matrix.yml b/.github/workflows/build-toolchain-matrix.yml
index 8167827fd..6975f66b4 100644
--- a/.github/workflows/build-toolchain-matrix.yml
+++ b/.github/workflows/build-toolchain-matrix.yml
@@ -51,7 +51,6 @@ jobs:
sudo apt-get install libmpc-dev
sudo apt-get install squashfs-tools
# If there are other dependencies, we should add them here and make sure the documentation is updated!
- continue-on-error: true
- name: Install x-compile system build dependencies
if: ${{ matrix.target-platform == 'Windows-x86_64' }}
@@ -66,7 +65,6 @@ jobs:
with:
bundler-cache: true
ruby-version: '3.3'
- continue-on-error: true
- name: Install Package Creator
if: ${{ matrix.target-platform == 'Linux-x86_64' || matrix.target-platform == 'Linux-aarch64' }}
@@ -74,16 +72,15 @@ jobs:
echo installing jordansissel/fpm ruby package
gem install fpm
fpm --version
- continue-on-error: true
- name: Checkout Code
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
with:
fetch-depth: 1 # we only require the last check-in, unless we want to create a changelog.
# Cache and restore dependencies instead of downloading them to increase build speed.
# Expires after 7 days.
- - uses: actions/cache@v5
+ - uses: actions/cache@v6
id: cache
with:
path: |
@@ -120,7 +117,6 @@ jobs:
./build-gdb.sh
echo "Removing un-necessary content"
rm -rf ${N64_INST}/share/locale/*
- continue-on-error: true
# https://fpm.readthedocs.io/en/v1.15.0/getting-started.html
# TODO: add --deb-changelog with versions (like we do in release)
@@ -167,7 +163,6 @@ jobs:
--maintainer "$Package_Maintainer" \
$Package_Source_Directory=$Package_Installation_Directory/ \
$Package_Name-env.sh=/etc/profile.d/$Package_Name-env.sh
- continue-on-error: true
env:
Package_Source_Directory: ${{ runner.temp }}/${{ env.Install_Directory }}/
Package_Installation_Directory: /opt/${{ env.Install_Directory }}
@@ -188,7 +183,6 @@ jobs:
name: gcc-toolchain-mips64-${{ matrix.target-platform }}
path: |
${{ runner.temp }}/${{ env.Install_Directory }}
- continue-on-error: true
- name: Publish Linux-x86_64 Build Artifacts
if: ${{ matrix.target-platform == 'Linux-x86_64' }}
@@ -198,7 +192,6 @@ jobs:
path: |
./**/*.deb
./**/*.rpm
- continue-on-error: true
- name: Publish Linux-aarch64 Build Artifacts
if: ${{ matrix.target-platform == 'Linux-aarch64' }}
@@ -208,14 +201,13 @@ jobs:
path: |
./**/*.deb
./**/*.rpm
- continue-on-error: true
Publish-Toolchain:
runs-on: ubuntu-latest
needs: Build-Toolchain
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Download Windows-x86_64 artifact
id: download-windows-x86_64-artifact
diff --git a/.gitignore b/.gitignore
index 73f517536..7a0f11482 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,9 +9,12 @@ tools/dumpdfs/dumpdfs
tools/mkdfs/mkdfs
tools/mksprite/convtool
tools/mksprite/mksprite
-tools/n64sym
+tools/cpaktool/cpaktool
+tools/mkmaterial/mkmaterial
+tools/n64metadata
tools/n64tool
-tools/n64sym
+tools/n64sym/n64sym
+tools/n64sym/n64symdump
tools/**/*.exe
website/ref/
diff --git a/.tokeignore b/.tokeignore
new file mode 100644
index 000000000..abda34ce4
--- /dev/null
+++ b/.tokeignore
@@ -0,0 +1,48 @@
+# Used by scripts/tokei-git.sh to ignore vendored files/folders
+# Vendored third-party libraries and tools
+
+# FatFS - File system library
+src/fatfs/
+
+# LibCart - Cartridge library (vendored)
+src/libcart/
+
+# Opus audio codec
+src/audio/opus/
+
+# LibXM - XM music format library
+src/audio/libxm/
+
+# libsamplerate - Audio resampling library
+tools/audioconv64/libsamplerate/
+tools/audioconv64/vadpcm/
+tools/audioconv64/dr_wav.h
+tools/audioconv64/dr_mp3.h
+
+# Compression libraries
+tools/common/shrinkler/
+tools/common/lz4/
+tools/common/incbin/
+tools/common/apultra/
+tools/common/json.hpp
+tools/common/mips_elf.h
+
+# STB libraries
+tools/common/stb_ds.h
+tools/common/lodepng.c
+tools/common/lodepng.h
+tools/common/mips_elf.h
+tools/common/subprocess.h
+
+# MKFont - vendored libraries
+tools/mkfont/freetype/
+tools/mkfont/pthash/
+tools/mkfont/plutosvg/
+tools/mkfont/rect_pack/
+
+# Exoquant - Image quantization library
+tools/mksprite/exoquant.*
+
+# PL_MPEG - MPEG decoder
+src/video/pl_mpeg/
+src/video/h264_decoder/
diff --git a/Makefile b/Makefile
index 03de5e6f1..98d624b24 100755
--- a/Makefile
+++ b/Makefile
@@ -16,7 +16,7 @@ N64_INCLUDEDIR = $(CURDIR)/include
# for libdragon specifically via a make override.
N64_BACKTRACE_FILE_PREFIX=libdragon
-LIBDRAGON_CFLAGS = -I$(CURDIR)/src
+LIBDRAGON_CFLAGS = -I$(CURDIR)/src -D__LIBDRAGON_INTERNAL_BUILD
# Activate N64 toolchain for libdragon build
libdragon: CC=$(N64_CC)
@@ -28,54 +28,97 @@ libdragon: CXXFLAGS+=$(N64_CXXFLAGS) $(LIBDRAGON_CFLAGS)
libdragon: ASFLAGS+=$(N64_ASFLAGS) $(LIBDRAGON_CFLAGS)
libdragon: RSPASFLAGS+=$(N64_RSPASFLAGS) $(LIBDRAGON_CFLAGS)
libdragon: LDFLAGS+=$(N64_LDFLAGS)
-libdragon: libdragon.a libdragonsys.a
+libdragon: libdragon.a libdragonsys.a gen-version
libdragonsys.a: $(BUILD_DIR)/system.o
LIBDRAGON_OBJS += \
+ $(BUILD_DIR)/accounting.o \
+ $(BUILD_DIR)/profile.o \
$(BUILD_DIR)/n64sys.o \
+ $(BUILD_DIR)/scratch.o \
+ $(BUILD_DIR)/vaddr64.o \
+ $(BUILD_DIR)/mi_memset.o \
$(BUILD_DIR)/interrupt.o \
$(BUILD_DIR)/backtrace.o \
- $(BUILD_DIR)/fmath.o \
+ $(BUILD_DIR)/symtable.o \
$(BUILD_DIR)/dir.o \
$(BUILD_DIR)/inthandler.o \
$(BUILD_DIR)/entrypoint.o \
+ $(BUILD_DIR)/entropy.o \
+ $(BUILD_DIR)/rand.o \
+ $(BUILD_DIR)/utils.o \
$(BUILD_DIR)/debug.o \
$(BUILD_DIR)/debugcpp.o \
$(BUILD_DIR)/usb.o \
$(BUILD_DIR)/libcart/cart.o \
$(BUILD_DIR)/fatfs/ff.o \
$(BUILD_DIR)/fatfs/ffunicode.o \
+ $(BUILD_DIR)/fat.o \
$(BUILD_DIR)/rompak.o \
$(BUILD_DIR)/dragonfs.o \
$(BUILD_DIR)/audio.o \
+ $(BUILD_DIR)/vi.o \
+ $(BUILD_DIR)/eia608.o \
$(BUILD_DIR)/display.o \
$(BUILD_DIR)/surface.o \
$(BUILD_DIR)/console.o \
$(BUILD_DIR)/asset.o \
+ $(BUILD_DIR)/pifile.o \
+ $(BUILD_DIR)/ed64x.o \
+ $(BUILD_DIR)/ed64.o \
$(BUILD_DIR)/rtc.o \
+ $(BUILD_DIR)/rtc_internal.o \
$(BUILD_DIR)/graphics.o \
$(BUILD_DIR)/rdp.o \
$(BUILD_DIR)/rsp.o \
$(BUILD_DIR)/rsp_crash.o \
$(BUILD_DIR)/inspector.o \
$(BUILD_DIR)/sprite.o \
+ $(BUILD_DIR)/lspr3.o \
+ $(BUILD_DIR)/lspr1.o \
+ $(BUILD_DIR)/rsp_lspr1.o \
$(BUILD_DIR)/dma.o \
$(BUILD_DIR)/timer.o \
$(BUILD_DIR)/exception.o \
$(BUILD_DIR)/do_ctors.o \
- $(BUILD_DIR)/dlfcn.o
-
+ $(BUILD_DIR)/dlfcn.o \
+ $(BUILD_DIR)/hashtable.o \
+ $(BUILD_DIR)/string_hash.o \
+ $(BUILD_DIR)/model64.o \
+ $(BUILD_DIR)/a3d.o \
+ $(BUILD_DIR)/sram.o \
+ $(BUILD_DIR)/flashram.o \
+ $(BUILD_DIR)/ucontext.o \
+ $(BUILD_DIR)/ucontext_asm.o \
+ $(BUILD_DIR)/coroutine.o
+
+include $(SOURCE_DIR)/kernel/libdragon.mk
include $(SOURCE_DIR)/audio/libdragon.mk
+include $(SOURCE_DIR)/bb/libdragon.mk
+include $(SOURCE_DIR)/dd/libdragon.mk
include $(SOURCE_DIR)/joybus/libdragon.mk
+include $(SOURCE_DIR)/GL/libdragon.mk
+include $(SOURCE_DIR)/video/libdragon.mk
include $(SOURCE_DIR)/rspq/libdragon.mk
include $(SOURCE_DIR)/rdpq/libdragon.mk
+include $(SOURCE_DIR)/magma/libdragon.mk
+include $(SOURCE_DIR)/math/libdragon.mk
include $(SOURCE_DIR)/compress/libdragon.mk
+# TODO: Make this generically available in n64.mk somehow
+$(SOURCE_DIR)/magma/rsp_magma.h: $(BUILD_DIR)/magma/rsp_magma.o
+ $(N64_OBJDUMP) -t $(BUILD_DIR)/magma/rsp_magma.elf \
+ | awk 'BEGIN {print("#ifndef __RSP_MAGMA_SYMBOLS\n#define __RSP_MAGMA_SYMBOLS") } $$3 ~ /\.data|\.text/ {printf("#define RSP_MAGMA_%s 0x%s\n", $$5, substr($$1,5,4))} END {print("#endif")}' \
+ > $@
+
+$(BUILD_DIR)/magma/magma.o: $(SOURCE_DIR)/magma/rsp_magma.h
+
libdragon.a: $(LIBDRAGON_OBJS)
%.a:
@echo " [AR] $@"
+ rm -f $@
$(N64_AR) -rcs -o $@ $^
examples:
@@ -95,25 +138,76 @@ tools-install:
tools-clean:
$(MAKE) -C tools clean
-install-mk: $(INSTALLDIR)/include/n64.mk
+# Install n64.mk to the install directory, always. This phony target does not
+# do any check and will always copy the file. This is what we need because the
+# installed file can be newer (timestamp wise) but different (eg: coming from
+# another branch).
+install-mk:
+ @echo " [INSTALL] n64.mk"
+ mkdir -p $(INSTALLDIR)/include
+ install -cv -m 0644 n64.mk $(INSTALLDIR)/include/n64.mk
+# This target is just a convenience target to install n64.mk in case it doesn't
+# exist yet, and it's only used by the clean targets that would otherwise fail.
+# This also allows to try running those targets without sudo, as a copy isn't
+# always required.
$(INSTALLDIR)/include/n64.mk: n64.mk
-# Always update timestamp of n64.mk. This make sure that further targets
-# depending on install-mk won't always try to re-install it.
mkdir -p $(INSTALLDIR)/include
install -cv -m 0644 n64.mk $(INSTALLDIR)/include/n64.mk
+gen-version:
+# Generate a version file for libdragon. We go through git archive so that
+# the export-subst is applied to the template file.
+# If .git doesn't exist, assume export-subst ran and just copy the file.
+# Otherwise, use git-archive to generate the subst'd version file.
+# NOTE: git can fail to access the repository via sudo for security/permissions
+# reasons (for instance, it happens on Mac when using sudo on an external volume).
+# We check for this via git rev-parse. In these cases, we hope the file was
+# generated without sudo as part of the normal build process.
+ @mkdir -p $(BUILD_DIR)
+ if [ -e .git ]; then \
+ if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then \
+ git archive --format=tar HEAD libdragon.version \
+ | tar -xOf - libdragon.version > "$(BUILD_DIR)/libdragon.version"; \
+ if ! git diff-index --quiet HEAD -- 2>/dev/null; then \
+ sed 's/"dirty":[[:space:]]*false/"dirty": true/' "$(BUILD_DIR)/libdragon.version" > "$(BUILD_DIR)/version.tmp"; \
+ mv -f "$(BUILD_DIR)/version.tmp" "$(BUILD_DIR)/libdragon.version"; \
+ fi; \
+ else \
+ if [ ! -e "$(BUILD_DIR)/libdragon.version" ]; then \
+ echo "WARNING: .git exists but git refuses to access it (permission problems?)" >&2; \
+ echo "WARNING: libdragon.version will not be generated." >&2; \
+ fi; \
+ fi; \
+ else \
+ cp libdragon.version "$(BUILD_DIR)/libdragon.version"; \
+ fi;
+
install: install-mk libdragon
+ @echo " [INSTALL] libdragon"
mkdir -p $(INSTALLDIR)/$(N64_TARGET)/lib
install -Cv -m 0644 libdragon.a $(INSTALLDIR)/$(N64_TARGET)/lib/libdragon.a
install -Cv -m 0644 n64.ld $(INSTALLDIR)/$(N64_TARGET)/lib/n64.ld
install -Cv -m 0644 dso.ld $(INSTALLDIR)/$(N64_TARGET)/lib/dso.ld
install -Cv -m 0644 rsp.ld $(INSTALLDIR)/$(N64_TARGET)/lib/rsp.ld
install -Cv -m 0644 libdragonsys.a $(INSTALLDIR)/$(N64_TARGET)/lib/libdragonsys.a
+ @echo " [INSTALL] libdragon.version"
mkdir -p $(INSTALLDIR)/$(N64_TARGET)/include
+ if [ -f "$(BUILD_DIR)/libdragon.version" ]; then \
+ install -Cv -m 0644 $(BUILD_DIR)/libdragon.version $(INSTALLDIR)/$(N64_TARGET)/include/; \
+ else \
+ rm -f $(INSTALLDIR)/$(N64_TARGET)/include/libdragon.version; \
+ fi;
+ @echo " [INSTALL] include/*.h"
install -Cv -m 0644 include/*.h $(INSTALLDIR)/$(N64_TARGET)/include/
install -Cv -m 0644 include/*.inc $(INSTALLDIR)/$(N64_TARGET)/include/
install -Cv -m 0644 include/ucode.S $(INSTALLDIR)/$(N64_TARGET)/include/
+ mkdir -p $(INSTALLDIR)/$(N64_TARGET)/include/GL
+ install -Cv -m 0644 include/GL/*.h $(INSTALLDIR)/$(N64_TARGET)/include/GL/
+ mkdir -p $(INSTALLDIR)/$(N64_TARGET)/include/newlib_overrides
+ install -Cv -m 0644 include/newlib_overrides/*.h $(INSTALLDIR)/$(N64_TARGET)/include/newlib_overrides/
+ mkdir -p $(INSTALLDIR)/$(N64_TARGET)/include/newlib_overrides/sys
+ install -Cv -m 0644 include/newlib_overrides/sys/*.h $(INSTALLDIR)/$(N64_TARGET)/include/newlib_overrides/sys/
mkdir -p $(INSTALLDIR)/$(N64_TARGET)/include/libcart
install -Cv -m 0644 src/libcart/cart.h $(INSTALLDIR)/$(N64_TARGET)/include/libcart/cart.h
mkdir -p $(INSTALLDIR)/$(N64_TARGET)/include/fatfs
@@ -132,13 +226,17 @@ regen:
test:
$(MAKE) -C tests
+ $(MAKE) -C tests/cpakfs test
+ $(MAKE) -C tests/hashtable test
+ $(MAKE) -C tests/preview test
+ python3 -m unittest discover tools/cpaktool/tests
-test-clean: install-mk
+test-clean: $(INSTALLDIR)/include/n64.mk
$(MAKE) -C tests clean
clobber: clean examples-clean tools-clean test-clean
-.PHONY : clobber clean doxygen-api examples examples-clean tools tools-clean tools-install test test-clean install-mk
+.PHONY : clobber clean doxygen-api examples examples-clean tools tools-clean tools-install test test-clean install-mk libdragon gen-version
# Automatic dependency tracking
-include $(wildcard $(BUILD_DIR)/*.d) $(wildcard $(BUILD_DIR)/*/*.d)
diff --git a/README.md b/README.md
index d2d87cbbd..5ffbe014c 100644
--- a/README.md
+++ b/README.md
@@ -1,115 +1,116 @@
-# Libdragon
-
-
-
+
+
-[](https://github.com/n64brew/libdragon/actions/workflows/build-toolchain-library-and-roms.yml)
+
+ A modern open-source SDK for Nintendo 64 development.
+
-## Welcome to libdragon
+
+
+
+
+
> [!TIP]
-> Coming back here after a while? Check the [ChangeLog](https://github.com/n64brew/libdragon/wiki/Stable-branch--Changelog) of our stable branch, or the [Preview branch](https://github.com/n64brew/libdragon/wiki/Preview-branch)
-
-Libdragon is an open-source SDK for Nintendo 64. It aims for a complete N64
-programming experience while providing programmers with modern approach to
-programming and debugging. These are the main features:
-
-* Based on modern GCC (version 16) and Newlib, for a full C11 programming experience.
- A Docker container is available to quickly set up the programming environment.
-* The GCC toolchain is 64 bit capable to be able to use the full R4300 capabilities
- (commercial games and libultra are based on a 32-bit ABI and is not possible
- to use 64-bit registers and opcodes with it)
-* Can be developed with newer-generation emulators (Ares) and development cartridges
- (64drive, EverDrive64, SummerCart64).
-* Support both vanilla N64 and iQue Player (Chinese variant). It is possible
- to run ROMs built with libdragon on iQue without modifying the source code.
-* 2D accelerated graphics:
- * Comprehensive RDP library called [rdpq](https://github.com/n64brew/libdragon/wiki/Rdpq)
- that offers both low-level access and very high-level blitting functions.
- * Support for drawing sprites of arbitrary sizes and arbitrary pixel formats.
- Rdpq takes care of handling TMEM limits transparently and efficiently.
- * Support for sprite zooming and rotation. Rotated sprites are transparently
- drawn via triangles instead of rectangles.
- * Support for all RDP pixel formats, including palettized ones.
- * Very simple render mode configuration, that allows for full RDP graphic effects
- including custom color combiner and blender.
- * Comprehensive [mksprite](https://github.com/n64brew/libdragon/wiki/Mksprite)
- tool, that converts from PNG format, includes optional state-of-the-art color
- quantizer and dithering.
- * Transparent compression of graphics for minimal ROM size
-* Audio:
- * Advanced RSP-accelerated mixer library, supporting up to 32 channels and
- streaming samples from ROM during playback for very low memory usage.
- * Supports WAV files for sound effects
- * Supports streaming of uncompressed or VADPCM-compressed WAV files for music.
- * Supports streaming using the state-of-the-art [Opus codec](https://github.com/n64brew/libdragon/wiki/Opus-decompression),
- for incredibly high compression ratio at realtime playback rate.
- * Supports playing of XM modules (FastTracker, MilkyTracker, OpenMPT). Can
- playback a 10-channel XM with < 3% CPU and < 10% RSP.
- * Supports playing of YM modules (Arkos Tracker 2)
-* Filesystems:
- * In-ROM filesystem implementation for assets. Assets can be loaded with
- `fopen("rom://asset.dat")` without having to do complex things to link them in.
- * SD card access (`fopen("sd://asset.dat")`) on all available flashcarts.
-* [Data compression](https://github.com/n64brew/libdragon/wiki/Compression):
- * Asset library for fast, transparent compression support for data files,
- including your custom ones.
- * Automatically integrated in conversion tools for graphics.
- * Three different compression algorithms with increasing compression ratio
- (and decreasing decompression speed). Currently based on LZ4, Aplib, Shrinkler.
- Compression ratios competitive with gzip and xz, at higher decompression speeds.
- * Optimized decompression routines in MIPS assembly that run in parallel
- with DMA for maximum speed.
- * Support for streaming decompression based on the `fopen()` interface.
-* [Dynamic library support](https://github.com/n64brew/libdragon/wiki/DSO-(dynamic-libraries))
- (DSO, sometimes called "overlays") for dynamically loading and unloading part of
- game code and data. This is implemented using the standard `dlopen()` / `dlsym()`.
-* Debugging:
- * Clear error screens with symbolized stack traces in case of crashes
- * Codebase is filled with assertions, so that you get a nice error screen
- instead of a console lockup.
- * Printf-debugging via `debugf()` which are redirected to your PC console
- in emulators and to USB via compatible tools (UNFLoader, g64drive).
-* Support for standard N64 controllers and memory paks.
-* Support for saving to flashes and EEPROMs (including a mini EEPROM
- filesystem to simplify serialization of structures).
-* Improved boot using open-source IPL3 bootcode, which boots ROMs up to 5x
- faster and allows for compressed game code (using libdragon compression library).
-
-The [preview branch](https://github.com/n64brew/libdragon/wiki/Preview-branch) features
-many more features:
-
- * 3D graphics
- * Allow for easily plugging in 3D graphics pipelines, that can
- potentially even coexist in the same scene.
- * Included in libdragon: full [OpenGL 1.1 port](https://github.com/n64brew/libdragon/wiki/OpenGL-on-N64), together with custom
- N64 extensions for using RDP-specific features.
- * Third-party: [Tiny3D](https://github.com/HailToDodongo/tiny3d), a high-performance native
- 3D pipeline.
- * Both OpenGL and Tiny3D import model files from Blender via the GLTF format,
- and feature also an animation system with skinning support.
- * a [MPEG1 RSP-accelerated movie player](https://github.com/n64brew/libdragon/wiki/MPEG1-Player), for high-quality FMVs.
- * Expected performance for FMV: 320x240 movie at 800 Kbit/s at 20 fps
- * Very simple to use also for render-to-texture scenarios, where
- a movie is played back as part of a 3D scene or as background in
- a 2D game.
- * a RDP-accelerated text engine (rdpq_text), with direct conversion from TrueType.
- * Highly optimized atlas creations for low memory impact and high runtime efficiency
- * Support for outlining of fonts to improve contrast
- * Full layout engine (paragraphs, centering, word-wrapping, etc.)
- * Fully Unicode aware
- * Initial support for multithreading via custom real-time kernel
- * Preemptive threads with priority
- * Mutexes, condition variables, semaphores, queues
- * Support for C11 atomic variables and thread-local storage
- * Runtime stack overflow detection
- * NOTE: at this point, most of libdragon is not thread safe yet, so only
- basic things can be performed in threads.
-
-and much more. These features will eventually land to trunk, but you can start playing
-with them even today. Go the [preview branch doc](https://github.com/n64brew/libdragon/wiki/Preview-branch) for more information.
+> Coming back here after a while? Check the [ChangeLog](https://github.com/n64brew/libdragon/wiki/Stable-branch--Changelog) of our stable branch, or the [Preview branch](https://github.com/n64brew/libdragon/wiki/Preview-branch).
+
+Libdragon aims to provide a complete Nintendo 64 development experience, combining modern development tools, high-level libraries, and low-level access to the hardware.
+
+## Features at a glance
+
+
## Getting started: how to build a ROM
@@ -125,10 +126,15 @@ libdragon targets real N64 hardware and uses many advanced corners
of the hardware not used by old commercial games, and thus requires
a modern N64 emulator which focuses on full hardware emulation.
-At the moment, the only emulator that accurately emulates the hardware
-(and does not just focus on playing old classics) is [Ares](https://github.com/ares-emulator/ares). Ares requires a modern PC with a discrete
-GPU with Vulkan support.
+At the moment, the only emulators that accurately emulate the hardware
+(and does not just focus on playing old classics) are:
+* [Ares](https://github.com/ares-emulator/ares)
+* [Gopher64](https://github.com/gopher64/gopher64).
+Both require a modern PC with a discrete GPU with Vulkan support. Gopher64
+is more performant for gaming.
+
+Ares has more development-oriented features so it is suggested during development.
You can develop 99% of your game using libdragon and the Ares emulator,
and be confident that the game will correctly run on hardware as well.
Make sure to turn on the "Homebrew mode" in Ares to enable developer
@@ -148,34 +154,61 @@ the vendors are usually less feature-rich.
## Libdragon stable vs preview
-Currently, there are two main libdragon versions:
-
- * The **stable** version is the one in the `trunk`. Stable means that we strive never
- to break backward compatibility, that is we will never do changes in a way
- that will impede existing applications to successfully compile and work
- against a newer libdragon version. We feel this is important because otherwise
- we would fragment the homebrew ecosystem too much, and we would leave a trail
- of libdragon-based applications that can't be compiled anymore.
- * The **preview** version is the one in the `preview` branch. This is where most
- development happens first. In fact, features are developed, evolved and
- battle-tested here, before the APIs are stabilized and they are finally
- merged on the trunk. Applications that use the preview branch need to be aware
- that the APIs can break at any time (though we try to avoid *gratuitous* breakage).
+There is a single libdragon version, but not every API in it carries the same
+promise. Each API is either **stable** or **preview**:
+
+ * A **stable** API will not break backward compatibility. We will never change
+ it in a way that impedes existing applications from compiling and working
+ against a newer libdragon version. We feel this is important because
+ otherwise we would fragment the homebrew ecosystem too much, and we would
+ leave a trail of libdragon-based applications that can't be compiled anymore.
+ * A **preview** API is still being developed, evolved and battle-tested. It can
+ change or be removed at any time, without notice and without a deprecation
+ period (though we try to avoid *gratuitous* breakage). This is where most new
+ features start their life, before they are stabilized.
+
+Preview APIs are documented as such (look for the "Preview API" note in the
+[documentation](https://libdragon.dev/ref/index.html)), and they are locked by
+default: using one is a compile time error that names the API you tried to use.
+To unlock them, set `LIBDRAGON_PREVIEW` in your Makefile, *before* including
+`n64.mk`:
+
+```makefile
+LIBDRAGON_PREVIEW = 1 # allow preview APIs, but warn on each use
+# LIBDRAGON_PREVIEW = 2 # fully unlock preview APIs (no diagnostics)
+include $(N64_INST)/include/n64.mk
+```
+
+Accepted values: `0` (default, hard error), `1` (usable with a compiler
+warning), `2` (fully unlocked).
+
+The switch is a property of your project, not of your libdragon installation:
+the same installed toolchain builds both stable-only and preview-using projects,
+and you can flip it at any time.
+
+There is no separate build of libdragon for preview APIs, so enabling the switch
+never changes the behaviour or the memory layout of the stable APIs: it only
+changes which APIs you are allowed to call.
+
+When a preview API is considered settled, it is promoted to stable and the
+marking is simply removed. Nothing needs to change in your project, and you can
+drop `LIBDRAGON_PREVIEW` once you no longer use any preview API.
## Upgrading libdragon
-If you are upgrade the stable version, check the [ChangeLog](https://github.com/n64brew/libdragon/wiki/Stable-branch--Changelog)
-in the wiki to see latest changes that were merged into the stable version of libdragon.
+Check the [ChangeLog](https://github.com/n64brew/libdragon/wiki/Stable-branch--Changelog)
+in the wiki to see the latest changes to the stable APIs.
Also check the wiki page for [common hurdles in upgrading libdragon](https://github.com/n64brew/libdragon/wiki/Upgrade-troubleshooting).
-If you are upgrading the preview version, instead, remember that some breaking
-changes are expected. We do not keep track of those though, so you will have
-to check the relevant header files yourself to check what is changed.
+If your project enables `LIBDRAGON_PREVIEW`, instead, remember that some
+breaking changes are expected. We do not keep track of those though, so you will
+have to check the relevant header files yourself to see what has changed.
## Resources
* [API reference](https://libdragon.dev/ref/topics.html)
* [Examples](https://github.com/n64brew/libdragon/tree/trunk/examples)
+ * [ROM Metadata Viewer](https://dragonminded.github.io/libdragon/static/metadata.html)
* [Wiki](https://github.com/n64brew/libdragon/wiki) (contains tutorials
and troubleshooting guides)
* [Discord n64brew](https://discord.gg/WqFgNWf)
diff --git a/boot/Makefile b/boot/Makefile
index 57680fcdc..b6b978675 100644
--- a/boot/Makefile
+++ b/boot/Makefile
@@ -37,7 +37,7 @@ N64_ASFLAGS += -mabi=32 -mgp32 -mfp32 -msingle-float -G0
N64_RSPASFLAGS = -march=mips1 -mabi=32 -Wa,--fatal-warnings
N64_LDFLAGS = -Wl,-T$(IPL3_LDSCRIPT) -Wl,-Map=build/ipl3.map -Wl,--gc-sections
-OBJS = build/ipl3.o build/minidragon.o build/rdram.o build/ique_trampoline.o build/entropy.o
+OBJS = build/ipl3.o build/minidragon.o build/rdram.o build/ique_trampoline.o build/entropy.o build/cleanup.o
ifeq ($(PROD),1)
OBJS += build/loader.o
diff --git a/boot/README.md b/boot/README.md
index fdcd1e4ba..63432b227 100644
--- a/boot/README.md
+++ b/boot/README.md
@@ -48,6 +48,20 @@ actually loading the main binary and run it.
(for each version, the md5 of ipl3_prod.z64 is reported)
+r10 (1337f98513cb0eb17ccd908f5866348b)
+* Allow boot on consoles with more than 8 MiB of RDRAM, by completely ignoring
+ extra chips. This is the only possible way, as the RCP simply misbehaves when
+ more than 8 MiB are configured.
+
+r9 (4ed3e5fd564235a844af74366dba7f92)
+* Stop using auto current calibration in RAC (for output current) as it
+ seems to be the cause for random corruptions when transferring long
+ sequences of 1s via RSP DMA. See https://github.com/rasky/n64_corruption_bug.
+* Improve entropy collection on warm boots by storing some randomness in low
+ RDRAM area (hopefully not destroyed by the running application).
+* Add a fatal error in case an ELF segment doesn't fit RDRAM
+* Remove spurious cache invalidations on high RDRAM that were leftovers.
+
r8 (419c213307cdb855934852c67759102b)
* Add ROM type to bootflags (byte 8). This will allow in the future to use
IPL3 as part of a replacement 64DD IPL.
@@ -223,7 +237,7 @@ This will create a file called `ipl3_prod.z64`. This contains the
non-debug version of IPL3. This version must be correctly signed
before being usable on real hardware and on accurate emulators.
To sign the ROM, the best option is to perform GPU cracking using
-[ipl3hasher](https://github.com/awygle/ipl3hasher).
+[ipl3hasher](https://github.com/Polprzewodnikowy/ipl3hasher-new).
After you have correctly signed the ROM, you can follow the same
instructions above to use it: you can either set `N64_ROM_HEADER`,
@@ -283,9 +297,6 @@ ELF file. If your application require sections to be available at runtime,
then you will need to handle compression in some other means (or modify
`n64elfcompress`).
-NOTE: at the moment of writing, `n64elfcompress` does not support ELFs with
-headers in 64-bit format.
-
#### Format details
A compressed ELF contains one or more segments which are made of compressed data.
diff --git a/boot/bin/ipl3_compat.z64 b/boot/bin/ipl3_compat.z64
index 622270fac..3898c0a84 100755
Binary files a/boot/bin/ipl3_compat.z64 and b/boot/bin/ipl3_compat.z64 differ
diff --git a/boot/bin/ipl3_dev.z64 b/boot/bin/ipl3_dev.z64
index 640800b30..9c015f553 100755
Binary files a/boot/bin/ipl3_dev.z64 and b/boot/bin/ipl3_dev.z64 differ
diff --git a/boot/bin/ipl3_prod.z64 b/boot/bin/ipl3_prod.z64
index 1702dfb90..48eeb3bb2 100755
Binary files a/boot/bin/ipl3_prod.z64 and b/boot/bin/ipl3_prod.z64 differ
diff --git a/boot/cleanup.c b/boot/cleanup.c
new file mode 100644
index 000000000..4f37c7264
--- /dev/null
+++ b/boot/cleanup.c
@@ -0,0 +1,93 @@
+/**
+ * @file cleanup.c
+ * @author thekovic
+ * @brief IPL3: Stage 3 (Cleanup)
+ *
+ * This module implements the third and final stage of the loader,
+ * which is responsible for cleaning up after the previous stages.
+ * It runs directly from ROM so that we are free to clean up our breadcrumbs
+ * in both DMEM and RDRAM.
+ *
+ * This stage runs from "high RDRAM", that is, it is placed at the end of RDRAM.
+ * The code is compiled to be relocatable via a trick in the Makefile, so that
+ * it can be placed at dynamic addresses (though normally only two would be
+ * possible: either near 4 MiB or 8 MiB).
+ *
+ * The tasks performed by this stage are:
+ *
+ * * Notify the PIF that the boot process is finished (in COMPAT mode,
+ * this is skipped because the game is expected to do it instead).
+ * * Clear DMEM except the boot flags area (in COMPAT mode, all of DMEM is cleared).
+ * * Jump to the entrypoint.
+ */
+
+#include "minidragon.h"
+#include "loader.h"
+
+// Inform PIF that the boot process is finished. If this is not written,
+// the PIF will halt the CPU after 5 seconds. This is not done by official
+// IPL3 but rather left to the game to do, but for our open source IPL3,
+// it seems better to leave it to the IPL3.
+static inline void pif_terminate_boot(void)
+{
+ si_write(0x7FC, 0x8);
+}
+
+// This is the last stage of IPL3. It runs directly from ROM so that we are
+// free of cleaning up our breadcrumbs in both DMEM and RDRAM.
+__attribute__((far, noreturn))
+void stage3(uint32_t entrypoint)
+{
+#ifndef COMPAT
+ // Notify the PIF that the boot process is finished. This will take a while
+ // so start it in background.
+ pif_terminate_boot();
+
+ // Read memory size from boot flags
+ int memsize = *(volatile uint32_t*) 0xA4000000;
+#else
+ int memsize = *(volatile uint32_t*) 0x80000318;
+#endif
+
+ // Reset the CPU cache, so that the application starts from a pristine state
+ cop0_clear_cache();
+
+ // Clear the reserved portion of RDRAM. To create a SP_WR_LEN value that works,
+ // we assume the reserved size is a multiple of 1024. It can be made to work
+ // also with other sizes, but this code will need to be adjusted.
+ while (*SP_DMA_FULL) {}
+ *SP_RSP_ADDR = 0xA4001000;
+ *SP_DRAM_ADDR = memsize - TOTAL_RESERVED_SIZE;
+ _Static_assert((TOTAL_RESERVED_SIZE % 1024) == 0, "TOTAL_RESERVED_SIZE must be multiple of 1024");
+ *SP_WR_LEN = (((TOTAL_RESERVED_SIZE >> 10) - 1) << 12) | (1024-1);
+
+ // Clear DMEM (leave only the boot flags area intact). Notice that we can't
+ // call debugf anymore after this, because a small piece of debugging code
+ // (io_write) is in DMEM, so it can't be used anymore.
+ while (*SP_DMA_FULL) {}
+ *SP_DRAM_ADDR = 0x00802000; // Area > 8 MiB which is guaranteed to be empty
+#ifndef COMPAT
+ *SP_RSP_ADDR = 0xA4000010;
+ *SP_RD_LEN = 4096-16-1;
+
+ // Wait until the PIF is done. This will also clear the interrupt, so that
+ // we don't leave the interrupt pending when we go to the entrypoint.
+ si_wait();
+#else
+ *SP_RSP_ADDR = 0xA4000000;
+ *SP_RD_LEN = 4096-1;
+#endif
+
+ // RSP DMA is guaranteed to be finished by now because stage3 is running from
+ // ROM and it's very slow. Anyway, let's just wait to avoid bugs in the future,
+ // because we don't want to begin using the stack (at the end of RDRAM) before it's finished.
+ while (*SP_DMA_BUSY) {}
+
+#ifndef COMPAT
+ // Configure SP at the end of RDRAM. This is a good default in general,
+ // then of course userspace code is free to reconfigure it.
+ asm ("move $sp, %0" : : "r" (0x80000000 + memsize - 0x10));
+#endif
+
+ goto *(void*)entrypoint;
+}
diff --git a/boot/entropy.c b/boot/entropy.c
index 66124460e..19838a275 100644
--- a/boot/entropy.c
+++ b/boot/entropy.c
@@ -4,7 +4,7 @@
* @brief Entropy accumulator
*
* This module implements a simple entropy accumulator. During IPL3, we
- * accumulate entropy from several sources and accumulate it using entropy_add.
+ * collect entropy from several sources and accumulate it using entropy_add.
* After we are done, we call entropy_get to retrieve the accumulated entropy
* in the form of a random 32-bit integer.
*
diff --git a/boot/entropy.h b/boot/entropy.h
index 2ee27b569..c2b807195 100644
--- a/boot/entropy.h
+++ b/boot/entropy.h
@@ -13,6 +13,12 @@
#define ENTROPY_API
#endif
+// Before boot, store the entropy state in low-mem. We will try as best-effort
+// to recover it after warm boot (assuming it wasn't destroyed by the running
+// application) so that we don't start from scatch (especially given that
+// warm boots don't perform RDRAM init so the entropy pool is low).
+#define RDRAM_ENTROPY_STATE ((volatile uint32_t*)0x800001A4)
+
register uint32_t __entropy_state asm("k1");
static inline void entropy_init(void)
diff --git a/boot/ipl3.c b/boot/ipl3.c
index e5e0e77c2..41e4d9773 100644
--- a/boot/ipl3.c
+++ b/boot/ipl3.c
@@ -264,8 +264,9 @@ void stage1(void)
entropy_init();
usb_init();
debugf("Libdragon IPL3");
-
+
entropy_add(C0_COUNT());
+ entropy_add(*DP_CLOCK);
C0_WRITE_CAUSE(0);
C0_WRITE_COUNT(0);
C0_WRITE_COMPARE(0);
@@ -320,6 +321,12 @@ void stage1(void)
}
}
+ // If this is a warm boot, try to add entropy stored by the previous IPL3
+ // run. This is just a best-effort; if it was destroyed, we'll just fetch
+ // whatever is there.
+ if (ipl2_resetType == 1)
+ entropy_add(*RDRAM_ENTROPY_STATE);
+
// Clear memory. Skip the first 0x400 bytes of RAM because it
// historically contains some boot flags that some existing code
// might expect to stay there.
diff --git a/boot/ipl3_compat.ld b/boot/ipl3_compat.ld
index 5cae0be81..2db31dcab 100644
--- a/boot/ipl3_compat.ld
+++ b/boot/ipl3_compat.ld
@@ -26,8 +26,6 @@ SECTIONS {
} > dmem
.text.stage23 0xB0000000 + SIZEOF(.text.dmem) : AT ( 0xA4000000 + SIZEOF(.text.dmem) ) {
- KEEP(build/loader_compat.o(.text.stage3))
- . = ALIGN(8);
__stage2_start = .;
LONG(__stage2_end - __stage2_start);
LONG(0);
@@ -36,6 +34,7 @@ SECTIONS {
KEEP(build/loader_compat.o(.data .data.*))
. = ALIGN(8);
__stage2_end = .;
+ KEEP(build/cleanup.o(.text .text.* .data .rodata .rodata.*))
*(.bss)
} > rom
diff --git a/boot/ipl3_dev.ld b/boot/ipl3_dev.ld
index 6909f5308..355fc94a5 100644
--- a/boot/ipl3_dev.ld
+++ b/boot/ipl3_dev.ld
@@ -41,6 +41,7 @@ SECTIONS {
KEEP(build/loader.o(.data .data.*))
. = ALIGN(8);
__stage2_end = .;
+ KEEP(build/cleanup.o(.text .text.* .data .rodata .rodata.*))
build/debug.o (.text .text.* .data)
*(.rodata)
*(.rodata.*)
diff --git a/boot/ipl3_prod.ld b/boot/ipl3_prod.ld
index 4fd8259db..5275770fe 100644
--- a/boot/ipl3_prod.ld
+++ b/boot/ipl3_prod.ld
@@ -36,8 +36,6 @@ SECTIONS {
KEEP(*(.text.ique_trampoline))
}
.text.stage23 0xB0001040 : AT ( 0xA4001040 ) {
- KEEP(build/loader.o(.text.stage3))
- . = ALIGN(8);
__stage2_start = .;
LONG(__stage2_end - __stage2_start);
LONG(0);
@@ -46,5 +44,6 @@ SECTIONS {
KEEP(build/loader.o(.data .data.*))
. = ALIGN(8);
__stage2_end = .;
+ KEEP(build/cleanup.o(.text .text.* .data .rodata .rodata.*))
} > rom
}
diff --git a/boot/loader.c b/boot/loader.c
index ba9bc92fe..bfd434234 100644
--- a/boot/loader.c
+++ b/boot/loader.c
@@ -4,8 +4,7 @@
* @brief IPL3: Stage 2 (ELF loader)
*
* This module implements the second stage of the loader, which is responsible
- * of searching and loading the ELF file embedded in the ROM, and jumping
- * to the entrypoint.
+ * of searching and loading the ELF file embedded in the ROM.
*
* This stage runs from "high RDRAM", that is, it is placed at the end of RDRAM.
* The code is compiled to be relocatable via a trick in the Makefile, so that
@@ -20,9 +19,6 @@
* stored in the ELF file itself).
* * Reset the RCP hardware (SP, DP, MI, PI, SI, AI).
* * Finalize the entropy accumulator and store it in the boot flags.
- * * Notify the PIF that the boot process is finished.
- * * Clear DMEM (except the boot flags area).
- * * Jump to the entrypoint.
*/
#include "loader.h"
#include "minidragon.h"
@@ -50,8 +46,6 @@
// Stage 1 functions we want to reuse
__attribute__((far))
extern void rsp_bzero_async(uint32_t rdram, int size);
-__attribute__((far))
-extern void cop0_clear_cache(void);
__attribute__((far, noreturn))
void stage3(uint32_t entrypoint);
@@ -102,15 +96,6 @@ static void fast_bzero_range(void *mem, void *mem_end)
rsp_bzero_async((uint32_t)mem, size);
}
-static void pif_terminate_boot(void)
-{
- // Inform PIF that the boot process is finished. If this is not written,
- // the PIF will halt the CPU after 5 seconds. This is not done by official
- // IPL3 but rather left to the game to do, but for our open source IPL3,
- // it seems better to leave it to the IPL3.
- si_write(0x7FC, 0x8);
-}
-
static const unsigned char font[] = {
0x00, 0x00, 0x00, 0x00, 0x00,
0x7e, 0xa1, 0x99, 0x85, 0x7e, 0x84, 0x82, 0xff, 0x80, 0x80, 0xc1, 0xa1,
@@ -174,6 +159,15 @@ static const char MSG_ELF_OFFSET_NOT_ALIGNED[] = {
_('A'), _('L'), _('I'), _('G'), _('N'), _('E'), _('D'), 0
};
+// "ELF SEGMENT TOO LARGE"
+__attribute__((aligned(1)))
+static const char MSG_ELF_SEGMENT_TOO_LARGE[] = {
+ _('E'), _('L'), _('F'), _(' '),
+ _('S'), _('E'), _('G'), _('M'), _('E'), _('N'), _('T'), _(' '),
+ _('T'), _('O'), _('O'), _(' '),
+ _('L'), _('A'), _('R'), _('G'), _('E'), 0
+};
+
#undef _
__attribute__((noreturn))
@@ -246,11 +240,6 @@ void stage2(void)
{
debugf("Hello from RDRAM ", __builtin_frame_address(0));
- // Invalidate the stack1 area, where the first stage put its stack.
- // We don't need it anymore, and we don't want it to be flushed to RDRAM
- // that will be cleared anyway.
- data_cache_hit_invalidate(STACK1_BASE, STACK1_SIZE);
-
// Search for the ELF header. We search for a 256-byte aligned header
// starting at offset 0x1000 in the ROM area (after the IPL3).
// We search for 64 MiB of ROM space (takes only a couple of seconds)
@@ -271,6 +260,7 @@ void stage2(void)
// Store the ELF offset in the boot flags
*(uint32_t*)0xA400000C = elf_header << 8;
+ uint32_t ramsize = *(uint32_t*)0xA4000000;
// Check if the ELF is 32/64 bit, and if it's big/little endian
uint32_t elf_type = io_read32(elf_header + 0x4);
@@ -302,6 +292,7 @@ void stage2(void)
uint32_t vaddr = phdr[elf64 ? 5 : 2];
uint32_t paddr = phdr[elf64 ? 7 : 3];
uint32_t size = phdr[elf64 ? 9 : 4];
+ uint32_t memsize = phdr[elf64 ? 11 : 5];
uint32_t flags = phdr[elf64 ? 1 : 6];
if (phdr[0] == PT_N64_DECOMP) {
@@ -320,6 +311,8 @@ void stage2(void)
if (!size) continue;
+ debugf("Segment ", i, phdr[0], offset, vaddr, paddr, size, memsize, flags);
+
// Make sure we can do PI DMA
if ((vaddr % 8) != 0) {
debugf("ELF: vaddr is not 8-byte aligned in segment");
@@ -329,8 +322,10 @@ void stage2(void)
debugf("ELF: file offset is not 2-byte aligned in segment");
fatal(MSG_ELF_OFFSET_NOT_ALIGNED);
}
-
- debugf("Segment ", i, phdr[0], offset, vaddr, size, flags);
+ if ((flags & PF_N64_COMPRESSED ? paddr : vaddr) + memsize > 0x80000000 + ramsize) {
+ debugf("ELF: segment does not fit in RDRAM");
+ fatal(MSG_ELF_SEGMENT_TOO_LARGE);
+ }
// Load the segment into RDRAM. Notice that we don't need to clear
// extra size at the end of the segment (as specified by phdr[5])
@@ -374,58 +369,12 @@ void stage2(void)
// Reset the RCP hardware
rcp_reset();
- // Write the accumulated entropy to the pool
- *(uint32_t*)0xA4000004 = entropy_get();
+ // Write the accumulated entropy to the pool, and to the low-RDRAM location
+ uint32_t entropy = entropy_get();
+ *(uint32_t*)0xA4000004 = entropy;
+ *RDRAM_ENTROPY_STATE = entropy;
debugf("Boot flags: ", *(uint32_t*)0xA4000000, *(uint32_t*)0xA4000004, *(uint32_t*)0xA4000008, *(uint32_t*)0xA400000C);
// Jump to the ROM finish function
stage3(entrypoint);
}
-
-// This is the last stage of IPL3. It runs directly from ROM so that we are
-// free of cleaning up our breadcrumbs in both DMEM and RDRAM.
-__attribute__((far, noreturn))
-void stage3(uint32_t entrypoint)
-{
- // Notify the PIF that the boot process is finished. This will take a while
- // so start it in background.
- pif_terminate_boot();
-
- // Reset the CPU cache, so that the application starts from a pristine state
- cop0_clear_cache();
-
- // Read memory size from boot flags
- int memsize = *(volatile uint32_t*)0xA4000000;
-
- // Clear the reserved portion of RDRAM. To create a SP_WR_LEN value that works,
- // we assume the reserved size is a multiple of 1024. It can be made to work
- // also with other sizes, but this code will need to be adjusted.
- while (*SP_DMA_FULL) {}
- *SP_RSP_ADDR = 0xA4001000;
- *SP_DRAM_ADDR = memsize - TOTAL_RESERVED_SIZE;
- _Static_assert((TOTAL_RESERVED_SIZE % 1024) == 0, "TOTAL_RESERVED_SIZE must be multiple of 1024");
- *SP_WR_LEN = (((TOTAL_RESERVED_SIZE >> 10) - 1) << 12) | (1024-1);
-
- // Clear DMEM (leave only the boot flags area intact). Notice that we can't
- // call debugf anymore after this, because a small piece of debugging code
- // (io_write) is in DMEM, so it can't be used anymore.
- while (*SP_DMA_FULL) {}
- *SP_RSP_ADDR = 0xA4000010;
- *SP_DRAM_ADDR = 0x00802000; // Area > 8 MiB which is guaranteed to be empty
- *SP_RD_LEN = 4096-16-1;
-
- // Wait until the PIF is done. This will also clear the interrupt, so that
- // we don't leave the interrupt pending when we go to the entrypoint.
- si_wait();
-
- // RSP DMA is guaranteed to be finished by now because stage3 is running from
- // ROM and it's very slow. Anyway, let's just wait to avoid bugs in the future,
- // because we don't want to begin using the stack (at the end of RDRAM) before it's finished.
- while (*SP_DMA_BUSY) {}
-
- // Configure SP at the end of RDRAM. This is a good default in general,
- // then of course userspace code is free to reconfigure it.
- asm ("move $sp, %0" : : "r" (0x80000000 + memsize - 0x10));
-
- goto *(void*)entrypoint;
-}
diff --git a/boot/loader.h b/boot/loader.h
index a063e9b42..0c4ad6293 100644
--- a/boot/loader.h
+++ b/boot/loader.h
@@ -7,16 +7,9 @@
// The loader is run directly from RDRAM for speed. We reserve a portion at
// the end of RDRAM for it.
-#define LOADER_SIZE (28*1024)
+#define LOADER_SIZE (32*1024)
-// We put the first stage stack into the cache, at the same address as the loader.
-// We reserve 4 KiB to it.
-#define STACK1_SIZE (4*1024)
-
-#define TOTAL_RESERVED_SIZE (LOADER_SIZE + STACK1_SIZE)
-
-#define STACK1_BASE (void*)(0x80800000 - LOADER_SIZE - STACK1_SIZE)
-#define STACK1_TOP (void*)(0x80800000 - LOADER_SIZE)
+#define TOTAL_RESERVED_SIZE (LOADER_SIZE)
#define LOADER_BASE(memsize, stage2size) (void*)(0x80000000 + (memsize) - (stage2size))
#define STACK2_TOP(memsize, stage2size) (LOADER_BASE(memsize, stage2size) - 16)
diff --git a/boot/loader_compat.c b/boot/loader_compat.c
index 1eed64363..c11b72d58 100644
--- a/boot/loader_compat.c
+++ b/boot/loader_compat.c
@@ -5,7 +5,7 @@
*
* This module implements the "compatibility" version of the second stage of
* the loader, which is responsible for loading a flat binary from a fixed
- * ROM address and jumping to it.
+ * ROM address.
*/
#include "minidragon.h"
#include "loader.h"
@@ -36,11 +36,6 @@ static void pi_wait(void)
__attribute__((used))
void stage2(void)
{
- // Invalidate the stack1 area, where the first stage put its stack.
- // We don't need it anymore, and we don't want it to be flushed to RDRAM
- // that will be cleared anyway.
- data_cache_hit_invalidate(STACK1_BASE, STACK1_SIZE);
-
uint32_t entrypoint = io_read32(0x10000008);
uint32_t size = io_read32(0x10000010);
if (size == 0 || size > (8<<20) - (entrypoint & 0x1FFFFFFF) - TOTAL_RESERVED_SIZE)
@@ -55,31 +50,3 @@ void stage2(void)
// Jump to the ROM finish function
stage3(entrypoint);
}
-
-// This is the last stage of IPL3. It runs directly from ROM so that we are
-// free of cleaning up our breadcrumbs in both DMEM and RDRAM.
-__attribute__((far, noreturn))
-void stage3(uint32_t entrypoint)
-{
- // Read memory size from boot flags
- int memsize = *(volatile uint32_t*)0x80000318;
-
- // Reset the CPU cache, so that the application starts from a pristine state
- cop0_clear_cache();
-
- // Clear DMEM and RDRAM stage 2 area
- while (*SP_DMA_FULL) {}
- *SP_RSP_ADDR = 0xA4001000;
- *SP_DRAM_ADDR = memsize - TOTAL_RESERVED_SIZE;
- _Static_assert((TOTAL_RESERVED_SIZE % 1024) == 0, "TOTAL_RESERVED_SIZE must be multiple of 1024");
- *SP_WR_LEN = (((TOTAL_RESERVED_SIZE >> 10) - 1) << 12) | (1024-1);
- while (*SP_DMA_FULL) {}
- *SP_RSP_ADDR = 0xA4000000;
- *SP_DRAM_ADDR = 0x00802000; // Area > 8 MiB which is guaranteed to be empty
- *SP_RD_LEN = 4096-1;
-
- // Wait until the DMA is done
- while (*SP_DMA_BUSY) {}
-
- goto *(void*)entrypoint;
-}
diff --git a/boot/minidragon.h b/boot/minidragon.h
index 8f47d7155..d0d5a75c1 100644
--- a/boot/minidragon.h
+++ b/boot/minidragon.h
@@ -159,6 +159,8 @@ typedef uint64_t u_uint64_t __attribute__((aligned(1)));
#define DP_CLEAR_INTERRUPT 0x0800
#define AI_CLEAR_INTERRUPT 0
+#define DP_CLOCK ((volatile uint32_t*)0xA4100010)
+
#define UncachedAddr(x) ((void*)((uint32_t)(x) | 0x20000000))
#define cache_op(addr, op, linesize, length) ({ \
diff --git a/boot/rdram.c b/boot/rdram.c
index 0799678ca..8a30852a4 100644
--- a/boot/rdram.c
+++ b/boot/rdram.c
@@ -327,11 +327,15 @@ static int rdram_calibrate_current(uint16_t chip_id)
int rdram_init(void (*bank_found)(int chip_id, bool last))
{
- // Start current calibration. This is necessary to ensure the RAC outputs
- // the correct current value to talk to RDRAM chips.
- *RI_CONFIG = RI_CONFIG_AUTO_CALIBRATION; // Turn on the RI auto current calibration
- wait(0x100); // Wait for calibration
- *RI_CURRENT_LOAD = 0; // Apply the calibrated value
+ // Configure RI/RAC output current (towards RDRAM). This is necessary to
+ // allow communication with the chips.
+ // The automatic current calibration has a hardware bug (it might cause
+ // corruptions in burst data transfers when many 1s have previously been
+ // on the bus), so we instead use the manual mode, with a current value
+ // at the middle of the range.
+ *RI_CONFIG = 0x20;
+ wait(0x100); // Seems like this wait is necessary(?)
+ *RI_CURRENT_LOAD = 0; // Apply the current value
// Activate communication with RDRAM chips. We can't do this before current calibration
// as the chips might not be able to communicate correctly if the current is wrong.
@@ -384,6 +388,16 @@ int rdram_init(void (*bank_found)(int chip_id, bool last))
if (chip_id)
bank_found(chip_id-2, false);
+ // The RI only supports up to 4 chips, so stop here. If we tried to
+ // calibrate a 5th chip connected to the bus, we would confuse RI bank
+ // tracking logic and cause all sorts of problems that would eventually
+ // lead to a crash. We tried that.
+ if (chip_id >= 8) {
+ debugf("\nWARNING: more than 4 2-MiB RDRAM chips connected");
+ debugf("RI does not support more than 4, skipping remaining\n");
+ break;
+ }
+
// Calibrate the chip current. n64brew suggests to do 4 attempts here
// but our tests seem to indicate that results are really stable and
// a single attempt seems enough.
diff --git a/build.sh b/build.sh
index efc0d50f9..fde997ac8 100755
--- a/build.sh
+++ b/build.sh
@@ -6,9 +6,12 @@ IFS=$'\n\t'
# If --test is passed, run tests after building
TEST_MODE=false
+# If --no-examples is passed, don't build examples
+BUILD_EXAMPLES=true
while (( $# )); do
case "$1" in
--test) TEST_MODE=true; shift ;;
+ --no-examples) BUILD_EXAMPLES=false; shift ;;
*) break ;;
esac
done
@@ -64,11 +67,13 @@ if [ "$TEST_MODE" = true ]; then
makeWithParams test
fi
-# Build examples - libdragon must be already installed at this point,
-# so first clobber the build to make sure that everything works against the
-# installed version rather than using local artifacts.
-makeWithParams clobber
-makeWithParams examples
+if [ "$BUILD_EXAMPLES" = true ]; then
+ # Build examples - libdragon must be already installed at this point,
+ # so first clobber the build to make sure that everything works against the
+ # installed version rather than using local artifacts.
+ makeWithParams clobber
+ makeWithParams examples
+fi
echo
echo Libdragon built successfully!
diff --git a/doxygen-public.conf b/doxygen-public.conf
index 86bba63d8..14b48e4c3 100644
--- a/doxygen-public.conf
+++ b/doxygen-public.conf
@@ -61,13 +61,13 @@ PROJECT_BRIEF =
# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy
# the logo to the output directory.
-PROJECT_LOGO = website/logo2.png
+PROJECT_LOGO =
# With the PROJECT_ICON tag one can specify an icon that is included in the tabs
# when the HTML document is shown. Doxygen will copy the logo to the output
# directory.
-PROJECT_ICON = website/favicon2.ico
+PROJECT_ICON =
# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path
# into which the generated documentation will be written. If a relative path is
@@ -279,7 +279,7 @@ TAB_SIZE = 4
# with the commands \{ and \} for these it is advised to use the version @{ and
# @} or use a double escape (\\{ and \\})
-ALIASES =
+ALIASES = "preview=@xrefitem preview \"Preview API\" \"Preview APIs\" This API is part of the libdragon preview: it may change without notice. Set `LIBDRAGON_PREVIEW=1` (warnings) or `2` (silent) in your Makefile to use it."
# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources
# only. Doxygen will then generate output that is more tailored for C. For
@@ -1000,7 +1000,11 @@ EXCLUDE = ./src/audio/libxm/ \
./src/audio/opus \
./src/compress/lzh5.c \
./src/fatfs/ \
- ./src/libcart/
+ ./src/libcart/ \
+ ./src/video/pl_mpeg/ \
+ ./src/GL/ \
+ ./src/video/ \
+ ./include/GL/
# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
# directories that are symbolic links (a Unix file system feature) are excluded
@@ -2364,8 +2368,13 @@ INCLUDE_FILE_PATTERNS =
# recursively expanded use the := operator instead of the = operator.
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
-PREDEFINED = "__attribute__(x)=" \
- "_Static_assert(c,s)="
+PREDEFINED = "N64=" \
+ "__attribute__(x)=" \
+ "_Static_assert(c,s)=" \
+ "LIBDRAGON_PREVIEW=2" \
+ "LIBDRAGON_PREVIEW_HEADER=" \
+ "LIBDRAGON_PREVIEW_API=" \
+ "LIBDRAGON_PREVIEW_SYM="
# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this
# tag can be used to specify a list of macro names that should be expanded. The
diff --git a/dso.ld b/dso.ld
index 4934b5c9f..96170dd16 100644
--- a/dso.ld
+++ b/dso.ld
@@ -46,16 +46,16 @@ SECTIONS {
*(.data)
*(.data.*)
*(.gnu.linkonce.d.*)
+ /* Define 4 bytes of space for __dso_handle */
+ . = ALIGN(4);
+ __dso_handle = .;
+ LONG(0);
}
.sdata : {
*(.sdata)
*(.sdata.*)
*(.gnu.linkonce.s.*)
- /* Define 4 bytes of space for __dso_handle */
- . = ALIGN(4);
- __dso_handle = .;
- LONG(0);
}
.lit8 : {
diff --git a/examples/Makefile b/examples/Makefile
index 722fcbfd1..521ad9981 100644
--- a/examples/Makefile
+++ b/examples/Makefile
@@ -1,22 +1,37 @@
EXAMPLES += audioplayer
+EXAMPLES += biosensortest
+EXAMPLES += brew-volley
+#EXAMPLES += compression # Disabled due to missing assets
+EXAMPLES += controllertest
+EXAMPLES += cpakdemo
EXAMPLES += cpptest
-EXAMPLES += ctest
-EXAMPLES += dfsdemo
+EXAMPLES += ctest
+EXAMPLES += customfont
+#EXAMPLES += dfsdemo # Disabled for now because it requires libmikmod
EXAMPLES += eepromfstest
+EXAMPLES += fontdemo
+EXAMPLES += fontgallery
+EXAMPLES += gldemo
EXAMPLES += helloworld
EXAMPLES += joypadtest
+EXAMPLES += loadspritefromsd
+EXAMPLES += lossysprite
+EXAMPLES += meshviewer
+EXAMPLES += mghello
+EXAMPLES += micro-ui
EXAMPLES += mixertest
-EXAMPLES += cpaktest
-EXAMPLES += cpak-utest
-EXAMPLES += fontdemo
-EXAMPLES += fontgallery
+EXAMPLES += ovldemo
EXAMPLES += overlays
+EXAMPLES += pixelshader
EXAMPLES += rdpqdemo
EXAMPLES += rspqdemo
+EXAMPLES += rtctest
+EXAMPLES += savedoodle
EXAMPLES += spriteanim
EXAMPLES += test
EXAMPLES += timers
-EXAMPLES += vrutest
+EXAMPLES += videoplayer
+EXAMPLES += vifx
EXAMPLES += vtest
all: $(EXAMPLES)
diff --git a/examples/audioplayer/.gitignore b/examples/audioplayer/.gitignore
index ecc834d58..671533caf 100644
--- a/examples/audioplayer/.gitignore
+++ b/examples/audioplayer/.gitignore
@@ -1 +1,2 @@
-filesystem/
+filesystem*/
+assets-*/
diff --git a/examples/audioplayer/Makefile b/examples/audioplayer/Makefile
index 17cf8fa1e..e894fe45c 100644
--- a/examples/audioplayer/Makefile
+++ b/examples/audioplayer/Makefile
@@ -1,22 +1,34 @@
BUILD_DIR=build
+# This example uses various preview APIs including SF64 and MID64
+LIBDRAGON_PREVIEW = 2
include $(N64_INST)/include/n64.mk
OBJS = $(BUILD_DIR)/audioplayer.o
-assets_xm1 = $(wildcard assets/*.xm)
-assets_xm2 = $(wildcard assets/*.XM)
-assets_ym1 = $(wildcard assets/*.ym)
-assets_ym2 = $(wildcard assets/*.YM)
-assets_conv = $(addprefix filesystem/,$(notdir $(assets_xm1:%.xm=%.xm64))) \
- $(addprefix filesystem/,$(notdir $(assets_xm2:%.XM=%.xm64))) \
- $(addprefix filesystem/,$(notdir $(assets_ym1:%.ym=%.ym64))) \
- $(addprefix filesystem/,$(notdir $(assets_ym2:%.YM=%.ym64)))
+assets_xm = $(wildcard assets/*.xm) $(wildcard assets/*.XM)
+assets_ym = $(wildcard assets/*.ym) $(wildcard assets/*.YM)
+assets_mid = $(wildcard assets/*.mid) $(wildcard assets/*.MID)
+assets_sf2 = $(wildcard assets/*.sf2) $(wildcard assets/*.SF2)
-AUDIOCONV_FLAGS ?=
+assets_xm64 = $(patsubst assets/%.xm,filesystem/%.xm64,$(filter %.xm,$(assets_xm))) \
+ $(patsubst assets/%.XM,filesystem/%.xm64,$(filter %.XM,$(assets_xm)))
+assets_ym64 = $(patsubst assets/%.ym,filesystem/%.ym64,$(filter %.ym,$(assets_ym))) \
+ $(patsubst assets/%.YM,filesystem/%.ym64,$(filter %.YM,$(assets_ym)))
+assets_mid64 = $(patsubst assets/%.mid,filesystem/%.mid64,$(filter %.mid,$(assets_mid))) \
+ $(patsubst assets/%.MID,filesystem/%.mid64,$(filter %.MID,$(assets_mid)))
+assets_sf64 = $(patsubst assets/%.sf2,filesystem/%.sf64,$(filter %.sf2,$(assets_sf2))) \
+ $(patsubst assets/%.SF2,filesystem/%.sf64,$(filter %.SF2,$(assets_sf2)))
+
+assets_conv = $(assets_xm64) \
+ $(assets_ym64) \
+ $(assets_mid64) \
+ $(assets_sf64)
+
+#AUDIOCONV_FLAGS ?= --xm-ext-samples filesystem/samples
all: audioplayer.z64
-# Run audioconv64 on all XM/YM files under assets/
+# Run audioconv64 on all XM/YM/MID files under assets/
# We do this file by file, but we could even do it just once for the whole
# directory, because audioconv64 supports directory walking.
filesystem/%.xm64: assets/%.xm
@@ -37,6 +49,25 @@ filesystem/%.ym64: assets/%.YM
@echo " [AUDIO] $@"
$(N64_AUDIOCONV) $(AUDIOCONV_FLAGS) -o filesystem "$<"
+filesystem/%.mid64: assets/%.mid
+ @mkdir -p $(dir $@)
+ @echo " [AUDIO] $@"
+ @$(N64_AUDIOCONV) $(AUDIOCONV_FLAGS) -o filesystem "$<"
+filesystem/%.mid64: assets/%.MID
+ @mkdir -p $(dir $@)
+ @echo " [AUDIO] $@"
+ @$(N64_AUDIOCONV) $(AUDIOCONV_FLAGS) -o filesystem "$<"
+
+# Hardcoded GM bank for MID64 playback (samples stream from ROM).
+filesystem/%.sf64: assets/%.sf2
+ @mkdir -p $(dir $@)
+ @echo " [AUDIO] $@"
+ @$(N64_AUDIOCONV) --sf-compress 1 -o filesystem "$<"
+filesystem/%.sf64: assets/%.SF2
+ @mkdir -p $(dir $@)
+ @echo " [AUDIO] $@"
+ @$(N64_AUDIOCONV) --sf-compress 1 -o filesystem "$<"
+
# As an example, activate compression darkness.ym. The file will be smaller
# but it will be impossible to seek it in the player.
filesystem/darkness.ym64: AUDIOCONV_FLAGS += --ym-compress true
@@ -48,7 +79,7 @@ audioplayer.z64: N64_ROM_TITLE="Audio Player"
audioplayer.z64: $(BUILD_DIR)/audioplayer.dfs
clean:
- rm -rf $(BUILD_DIR) audioplayer.z64
+ rm -rf $(BUILD_DIR) audioplayer.z64 filesystem
-include $(wildcard $(BUILD_DIR)/*.d)
diff --git a/examples/audioplayer/assets/florestan-full.sf2 b/examples/audioplayer/assets/florestan-full.sf2
new file mode 100644
index 000000000..74f5e7dde
Binary files /dev/null and b/examples/audioplayer/assets/florestan-full.sf2 differ
diff --git a/examples/audioplayer/assets/skywards.xm b/examples/audioplayer/assets/skywards.xm
new file mode 100644
index 000000000..85ef8b65d
Binary files /dev/null and b/examples/audioplayer/assets/skywards.xm differ
diff --git a/examples/audioplayer/audioplayer.c b/examples/audioplayer/audioplayer.c
index ced88d2ef..892e3211e 100644
--- a/examples/audioplayer/audioplayer.c
+++ b/examples/audioplayer/audioplayer.c
@@ -4,6 +4,8 @@
// We need to show lots of internal details of the module which are not
// exposed via public API, so include the internal header file.
#include "../../src/audio/libxm/xm_internal.h"
+#include "../../src/audio/mixer_internal.h"
+#include "../../src/accounting_internal.h"
#define CLAMP(x, min, max) ((x) < (min) ? (min) : ((x) > (max) ? (max) : (x)))
@@ -23,9 +25,42 @@ int menu_sel = 0;
static char* songfiles[4096];
static int num_songs = 0;
+static char* bankfiles[64];
+static int num_banks = 0;
+static int bank_sel = 0;
+
+/** Mixer channels reserved for SF64 polyphony when playing MID64. */
+#define MID_VOICES 24
+
+static sf64_bank_t *g_sf64_bank;
+/** RDRAM held by #g_sf64_bank (heap delta across #sf64_load). */
+static int g_sf64_bank_ramsz;
+
+/** Heap bytes currently used (see #sys_get_heap_stats). */
+static int heap_used(void) {
+ heap_stats_t st;
+ sys_get_heap_stats(&st);
+ return st.used;
+}
+
+/** Load @p path as the current SF64 bank; updates #g_sf64_bank_ramsz. */
+static void load_sf64_bank(const char *path) {
+ int before = heap_used();
+ g_sf64_bank = sf64_load(path);
+ assertf(g_sf64_bank, "cannot load %s", path);
+ g_sf64_bank_ramsz = heap_used() - before;
+}
+
+/** Filename without "rom:/" prefix and extension (writes into @p out). */
+static void asset_basename(const char *path, char *out, int out_sz) {
+ strlcpy(out, path + 5, out_sz);
+ char *dot = strrchr(out, '.');
+ if (dot) *dot = '\0';
+}
+
static void draw_header(display_context_t disp) {
- graphics_draw_text(disp, 200-70, 10, "XM/YM Module Audio Player");
- graphics_draw_text(disp, 200-45, 20, "v2.0 - by Rasky");
+ graphics_draw_text(disp, 200-70, 10, "XM/YM/MID Audio Player");
+ graphics_draw_text(disp, 200-45, 20, "v2.1 - by Rasky");
}
static bool strendswith(const char *str, const char *suffix) {
@@ -61,17 +96,16 @@ enum Page page_intro(void) {
graphics_fill_screen(disp, 0);
draw_header(disp);
- graphics_draw_text(disp, 30, 50, "This player is capable of playing .XM/.YM modules,");
+ graphics_draw_text(disp, 30, 50, "This player plays .XM/.YM modules and .MID songs,");
graphics_draw_text(disp, 30, 58, "up to 32 channels and 48Khz, using an optimized");
graphics_draw_text(disp, 30, 66, "engine that uses little CPU and RSP time. ");
- graphics_draw_text(disp, 30, 80, "XM/YM files must first be converted into XM64/YM64,");
- graphics_draw_text(disp, 30, 88, "using the audioconv64 tool. This format is");
- graphics_draw_text(disp, 30, 96, "designed for native playback on N64.");
+ graphics_draw_text(disp, 30, 80, "Files are converted with audioconv64 into XM64,");
+ graphics_draw_text(disp, 30, 88, "YM64, or MID64. MIDI synthesis uses SF64 banks");
+ graphics_draw_text(disp, 30, 96, "(default: florestan-full). Press Z to cycle banks.");
- graphics_draw_text(disp, 30,112, "The player will stream most of the data");
- graphics_draw_text(disp, 30,120, "directly from the ROM, so also the amount of");
- graphics_draw_text(disp, 30,128, "RDRAM that will be used will be very little.");
+ graphics_draw_text(disp, 30,112, "Most sample data is streamed from ROM, so RDRAM");
+ graphics_draw_text(disp, 30,120, "usage stays modest even with large banks.");
graphics_draw_text(disp, 30,144, "Press START to begin!");
@@ -91,7 +125,7 @@ enum Page page_intro_error(void) {
display_context_t disp = display_get();
graphics_fill_screen(disp, 0);
draw_header(disp);
- graphics_draw_text(disp, 40, 50, "No .XM64 roms found in the filesystem");
+ graphics_draw_text(disp, 40, 50, "No XM64/YM64/MID64 files in the filesystem");
display_show(disp);
abort();
}
@@ -169,18 +203,27 @@ enum Page page_menu(void) {
enum Page page_song(void) {
char sbuf[1024];
- int64_t tot_time = 0, tot_cpu = 0, tot_rsp = 0, tot_dma = 0;
+ int64_t tot_time = 0, tot_cpu = 0, tot_rsp = 0, tot_dma = 0, tot_frames = 0;
+ int64_t avg_time = 0, avg_cpu = 0, avg_rsp = 0, avg_dma = 0;
+ int poly_max = 0, poly_disp = 0;
+ uint32_t poly_win = TICKS_READ();
int screen_first_inst = 0;
- enum SONG_TYPE { SONG_XM, SONG_YM };
+ enum SONG_TYPE { SONG_XM, SONG_YM, SONG_MID };
xm64player_t xm;
ym64player_t ym; ym64player_songinfo_t yminfo;
+ mid64player_t *mid = NULL;
+ sf64_synth_t *synth = NULL;
enum SONG_TYPE song_type;
const char *song_name; int song_channels;
- int song_romsz=0, song_ramsz=0;
+ int song_romsz = 0;
+ int heap_base = 0;
+ static char mid_name[64];
if (strendswith(cur_rom, ".ym64") || strendswith(cur_rom, ".YM64"))
song_type = SONG_YM;
+ else if (strendswith(cur_rom, ".mid64") || strendswith(cur_rom, ".MID64"))
+ song_type = SONG_MID;
else
song_type = SONG_XM;
@@ -190,31 +233,48 @@ enum Page page_song(void) {
dfs_close(fh);
}
+ // Everything allocated for this song (player, synth, mixer rings, …)
+ // is measured as a heap delta from here. The SF64 bank is shared across
+ // MID songs and is added separately below.
+ heap_base = heap_used();
+
debugf("Loading %s\n", cur_rom);
if (song_type == SONG_XM) {
xm64player_open(&xm, cur_rom);
xm64player_play(&xm, 0);
song_name = xm_get_module_name(xm.ctx);
song_channels = xm64player_num_channels(&xm);
-
- song_ramsz = sizeof(xm64player_t) + xm.ctx->ctx_size;
- #if XM_STREAM_PATTERNS
- song_ramsz -= xm.ctx->ctx_size_all_patterns;
- song_ramsz += xm.ctx->ctx_size_stream_pattern_buf;
+ #if 0
+ // Seek to a specific position in the song
+ // Isolate a specific channel
+ xm64player_seek(&xm, 8, 0, 0);
+ // for (int i=0;i<32;i++) {
+ // if (i != 0) {
+ // mute[i] = 1;
+ // xm_mute_channel(xm.ctx, i+1, 1);
+ // }
+ // }
#endif
- #if XM_STREAM_WAVEFORMS
- song_ramsz -= xm.ctx->ctx_size_all_samples;
- for (int i=0;i<32;i++)
- song_ramsz += xm.ctx->ctx_size_stream_sample_buf[i];
- #endif
- } else {
+ } else if (song_type == SONG_YM) {
ym64player_open(&ym, cur_rom, &yminfo);
ym64player_play(&ym, 0);
song_name = yminfo.name;
song_channels = 3;
wrap(yminfo.comment, 40);
- song_ramsz = sizeof(ym64player_t);
- if (ym.decoder) song_ramsz += sizeof(*ym.decoder);
+ } else {
+ assertf(g_sf64_bank, "MID64: SF64 bank not loaded");
+ synth = sf64_synth_create(g_sf64_bank);
+ sf64_synth_set_channels(synth, 0, MID_VOICES, MIXER_PRIORITY_MUSIC);
+ for (int i = 0; i < MID_VOICES; i++)
+ mixer_ch_set_limits(i, 0, 96000, 0);
+
+ mid = mid64player_load(cur_rom);
+ mid64player_set_loop(mid, true);
+ mid64player_play(mid, sf64_synth_midi_target(synth));
+
+ asset_basename(cur_rom, mid_name, sizeof(mid_name));
+ song_name = mid_name;
+ song_channels = SF64_MIDI_CHANNELS;
}
// Unmute all channels
@@ -231,9 +291,17 @@ enum Page page_song(void) {
sprintf(sbuf, "Song: %s", song_name);
graphics_draw_text(disp, 20, 50, sbuf);
- sprintf(sbuf, "Channels: %d", song_channels);
+ if (song_type == SONG_MID)
+ sprintf(sbuf, "MIDI channels: %d", song_channels);
+ else
+ sprintf(sbuf, "Channels: %d", song_channels);
graphics_draw_text(disp, 20, 60, sbuf);
+ // Live heap delta: mixer rings are allocated lazily on first use.
+ int song_ramsz = heap_used() - heap_base;
+ if (song_type == SONG_MID)
+ song_ramsz += g_sf64_bank_ramsz;
+ if (song_ramsz < 0) song_ramsz = 0;
sprintf(sbuf, "ROM: %d KiB | RDRAM: %d KiB", (song_romsz+512)/1024, (song_ramsz+512)/1024);
graphics_draw_text(disp, 20, 70, sbuf);
@@ -249,20 +317,19 @@ enum Page page_song(void) {
ym64player_tell(&ym, &pos, NULL);
sprintf(sbuf, "Pos: %04x/%04x\n", pos, len);
graphics_draw_text(disp, 280, 50, sbuf);
+ } else if (song_type == SONG_MID) {
+ uint32_t cur_ms = mid64player_tell_ms(mid);
+ uint32_t tot_ms = mid64player_get_duration_ms(mid);
+ sprintf(sbuf, "Time: %ld:%02ld / %ld:%02ld\n",
+ (long)(cur_ms / 1000 / 60), (long)((cur_ms / 1000) % 60),
+ (long)(tot_ms / 1000 / 60), (long)((tot_ms / 1000) % 60));
+ graphics_draw_text(disp, 280, 50, sbuf);
}
- if (tot_time) {
- float pcpu = (float)tot_cpu * 100.f / (float)tot_time;
- float prsp = (float)tot_rsp * 100.f / (float)tot_time;
- float pdma = (float)tot_dma * 100.f / (float)tot_time;
-
- sprintf(sbuf, "CPU: %.2f%% RSP: %.2f%%\n", pcpu, prsp);
- graphics_draw_text(disp, 280, 60, sbuf);
- sprintf(sbuf, "DMA: %.2f%%", pdma);
- graphics_draw_text(disp, 280, 70, sbuf);
-
- debugf("CPU: %.2f%% RSP: %.2f%% DMA: %.2f%%\n", pcpu, prsp, pdma);
- }
+ sprintf(sbuf, "CPU: %lldus\n", avg_time);
+ graphics_draw_text(disp, 280, 60, sbuf);
+ sprintf(sbuf, "Wait: DMA: %lldus RSP: %lldus", avg_dma, avg_rsp);
+ graphics_draw_text(disp, 280, 70, sbuf);
for (int i=0; i<32; i++) {
if (i == song_channels) break;
@@ -285,7 +352,7 @@ enum Page page_song(void) {
break;
graphics_draw_text(disp, 120, 120+i*10, xm.ctx->module.instruments[screen_first_inst+i].name);
}
- } else {
+ } else if (song_type == SONG_YM) {
// Display the YM song information (author and comment).
sprintf(sbuf, "Author: %s", yminfo.author);
graphics_draw_text(disp, 120, 120, sbuf);
@@ -299,39 +366,63 @@ enum Page page_song(void) {
ypos += 10;
line = strtok(NULL, "\n");
}
+ } else {
+ char bank_name[64];
+ asset_basename(bankfiles[bank_sel], bank_name, sizeof(bank_name));
+ sprintf(sbuf, num_banks > 1 ? "Bank: %s (Z: next)" : "Bank: %s", bank_name);
+ graphics_draw_text(disp, 120, 120, sbuf);
+ sprintf(sbuf, "PPQN: %d", mid64player_get_ppqn(mid));
+ graphics_draw_text(disp, 120, 130, sbuf);
+ sprintf(sbuf, "Tempo: %ld us/qn", (long)mid64player_get_tempo(mid));
+ graphics_draw_text(disp, 120, 140, sbuf);
+ sprintf(sbuf, "Duration: %ld ticks", (long)mid64player_get_duration_ticks(mid));
+ graphics_draw_text(disp, 120, 150, sbuf);
+ int poly = 0;
+ for (int i = 0; i < MID_VOICES; i++)
+ if (mixer_ch_playing(i)) poly++;
+ if (poly > poly_max) poly_max = poly;
+ if (TICKS_DISTANCE(poly_win, TICKS_READ()) >= TICKS_PER_SECOND / 2) {
+ poly_disp = poly_max;
+ poly_max = 0;
+ poly_win = TICKS_READ();
+ }
+ sprintf(sbuf, "Polyphony: %d / %d", poly_disp, MID_VOICES);
+ graphics_draw_text(disp, 120, 160, sbuf);
}
display_show(disp);
-
- tot_time = 0, tot_cpu = 0, tot_rsp = 0, tot_dma = 0;
+ profile_next_frame();
uint32_t start_play_loop = TICKS_READ();
- bool first_loop = true;
- int audiosz = audio_get_buffer_length();
- while (TICKS_DISTANCE(start_play_loop, TICKS_READ()) < TICKS_PER_SECOND)
+ //int audiosz = audio_get_buffer_length();
+ //while (TICKS_DISTANCE(start_play_loop, TICKS_READ()) < TICKS_PER_SECOND)
+ do
{
- extern int64_t __mixer_profile_rsp, __wav64_profile_dma;
- __mixer_profile_rsp = __wav64_profile_dma = 0;
-
- uint32_t t0 = TICKS_READ();
-
- while (!audio_can_write()) {}
-
- uint32_t t1 = TICKS_READ();
-
- int16_t *out = audio_write_begin();
- mixer_poll(out, audiosz);
- audio_write_end();
-
- uint32_t t2 = TICKS_READ();
-
- if (!first_loop) {
- tot_dma += __wav64_profile_dma;
- tot_rsp += __mixer_profile_rsp;
- tot_cpu += (t2-t1) - __mixer_profile_rsp - __wav64_profile_dma;
- tot_time += t2-t0;
+ uint32_t t1t = get_ticks_us();
+ uint32_t t1u = get_user_ticks();
+ uint64_t t1rsp = acct_get_ticks(ACCT_CAT_RSP) + acct_get_ticks(ACCT_CAT_RSPQ);
+ uint64_t t1dma = acct_get_ticks(ACCT_CAT_PI);
+
+ mixer_try_play();
+
+ uint32_t t2t = get_ticks_us();
+ uint32_t t2u = get_user_ticks();
+ uint64_t t2rsp = acct_get_ticks(ACCT_CAT_RSP) + acct_get_ticks(ACCT_CAT_RSPQ);
+ uint64_t t2dma = acct_get_ticks(ACCT_CAT_PI);
+
+ tot_dma += TICKS_TO_US(t2dma-t1dma);
+ tot_rsp += TICKS_TO_US(t2rsp-t1rsp);
+ tot_cpu += TICKS_TO_US(t2u-t1u);
+ tot_time += t2t-t1t;
+ tot_frames++;
+ if (tot_frames >= 60) {
+ avg_time = tot_time / tot_frames;
+ avg_cpu = tot_cpu / tot_frames;
+ avg_rsp = tot_rsp / tot_frames;
+ avg_dma = tot_dma / tot_frames;
+ tot_time = 0, tot_cpu = 0, tot_rsp = 0, tot_dma = 0, tot_frames = 0;
+ debugf("CPU: %lldus | Wait DMA: %lldus | Wait RSP: %lldus\n", avg_time, avg_dma, avg_rsp);
}
- first_loop = false;
joypad_poll();
joypad_buttons_t ckeys = joypad_get_buttons_pressed(JOYPAD_PORT_1);
@@ -371,6 +462,8 @@ enum Page page_song(void) {
mute[chselect] = !mute[chselect];
if (song_type == SONG_XM)
xm_mute_channel(xm.ctx, chselect+1, mute[chselect]);
+ else if (song_type == SONG_MID)
+ sf64_synth_mute_channel(synth, chselect, mute[chselect]);
break;
}
if (ckeys.c_up) {
@@ -380,25 +473,56 @@ enum Page page_song(void) {
mute[i] = !mute[chselect];
if (song_type == SONG_XM)
xm_mute_channel(xm.ctx, i+1, mute[i]);
+ else if (song_type == SONG_MID)
+ sf64_synth_mute_channel(synth, i, mute[i]);
}
break;
}
+ if (ckeys.z && song_type == SONG_MID && num_banks > 1) {
+ bank_sel = (bank_sel + 1) % num_banks;
+ mid64player_close(mid);
+ sf64_synth_close(synth);
+ for (int i = 0; i < MID_VOICES; i++)
+ mixer_ch_set_limits(i, 0, 0, 0);
+ sf64_close(g_sf64_bank);
+ load_sf64_bank(bankfiles[bank_sel]);
+ // Bank is already counted via g_sf64_bank_ramsz; reset the
+ // song baseline so mid/synth/mixer are measured afresh.
+ heap_base = heap_used();
+ synth = sf64_synth_create(g_sf64_bank);
+ sf64_synth_set_channels(synth, 0, MID_VOICES, MIXER_PRIORITY_MUSIC);
+ for (int i = 0; i < MID_VOICES; i++)
+ mixer_ch_set_limits(i, 0, 96000, 0);
+ mid = mid64player_load(cur_rom);
+ mid64player_set_loop(mid, true);
+ mid64player_play(mid, sf64_synth_midi_target(synth));
+ memset(mute, 0, sizeof(mute));
+ chselect = 0;
+ break;
+ }
+
if (ckeys.b) {
if (song_type == SONG_XM)
xm64player_close(&xm);
- else
+ else if (song_type == SONG_YM)
ym64player_close(&ym);
+ else {
+ mid64player_close(mid);
+ sf64_synth_close(synth);
+ for (int i = 0; i < MID_VOICES; i++)
+ mixer_ch_set_limits(i, 0, 0, 0);
+ }
return PAGE_MENU;
}
- }
+ } while (0);
}
}
int main(void) {
- joypad_init();
debug_init_emulog();
debug_init_usblog();
+ joypad_init();
display_init(RESOLUTION_512x240, DEPTH_16_BPP, 3, GAMMA_NONE, FILTERS_RESAMPLE);
dfs_init(DFS_DEFAULT_LOCATION);
@@ -407,26 +531,51 @@ int main(void) {
dir_t dir;
if (dir_findfirst("rom:/", &dir) == 0) do {
if (strendswith(dir.d_name, ".xm64") || strendswith(dir.d_name, ".XM64") ||
- strendswith(dir.d_name, ".ym64") || strendswith(dir.d_name, ".YM64")) {
+ strendswith(dir.d_name, ".ym64") || strendswith(dir.d_name, ".YM64") ||
+ strendswith(dir.d_name, ".mid64") || strendswith(dir.d_name, ".MID64")) {
sprintf(sbuf, "rom:/%s", dir.d_name);
songfiles[num_songs++] = strdup(sbuf);
}
+ if (strendswith(dir.d_name, ".sf64") || strendswith(dir.d_name, ".SF64")) {
+ sprintf(sbuf, "rom:/%s", dir.d_name);
+ bankfiles[num_banks++] = strdup(sbuf);
+ }
} while (dir_findnext("rom:/", &dir) == 0);
+ xm64_set_extsampledir("rom:/samples");
+
enum Page page = PAGE_INTRO;
if (num_songs == 0)
page = PAGE_INTRO_ERROR;
#if 0
- // Force immediately playback of a song
+ // Force immediately playback of a song (for mixer profiling).
page = PAGE_SONG;
- cur_rom = "rom:/Claustrophobia.xm64";
+ cur_rom = "rom:/BUTTERFL.xm64";
#endif
audio_init(44100, 4);
mixer_init(32);
+ // Default GM bank for MID64; Z cycles through every SF64 in rom:/.
+ for (int i = 0; i < num_banks; i++) {
+ if (strstr(bankfiles[i], "florestan-full")) {
+ bank_sel = i;
+ break;
+ }
+ }
+ assertf(num_banks > 0, "no SF64 banks in rom:/");
+ load_sf64_bank(bankfiles[bank_sel]);
+
+ profile_parms_t pparms = {
+ .num_slots = 32,
+ .dump_stderr_interval = 5.0f,
+ };
+ profile_init(&pparms);
+ __mixer_profile_init();
+ profile_set_target_fps(60.0f);
+
while(1) {
switch (page) {
case PAGE_INTRO: {
diff --git a/examples/biosensortest/Makefile b/examples/biosensortest/Makefile
new file mode 100644
index 000000000..dbd02e17b
--- /dev/null
+++ b/examples/biosensortest/Makefile
@@ -0,0 +1,19 @@
+all: biosensortest.z64
+.PHONY: all
+
+BUILD_DIR = build
+# This example uses the bio sensor API, which is a preview API
+LIBDRAGON_PREVIEW = 2
+include $(N64_INST)/include/n64.mk
+
+OBJS = $(BUILD_DIR)/biosensortest.o
+
+biosensortest.z64: N64_ROM_TITLE = "Bio Sensor Test"
+
+$(BUILD_DIR)/biosensortest.elf: $(OBJS)
+
+clean:
+ rm -rf $(BUILD_DIR) *.z64
+.PHONY: clean
+
+-include $(wildcard $(BUILD_DIR)/*.d)
diff --git a/examples/biosensortest/biosensortest.c b/examples/biosensortest/biosensortest.c
new file mode 100644
index 000000000..00741b100
--- /dev/null
+++ b/examples/biosensortest/biosensortest.c
@@ -0,0 +1,100 @@
+/**
+ * @file biosensortest.c
+ * @author Christopher Bonhage (me@christopherbonhage.com)
+ * @brief N64 test ROM for Bio Sensor subsystem
+ */
+
+#include
+#include
+
+#define HISTOGRAM_WIDTH 60
+
+typedef struct {
+ bool buffer[HISTOGRAM_WIDTH];
+ int index;
+} pulse_history_t;
+
+int main(void)
+{
+ joypad_buttons_t pressed;
+ pulse_history_t history[JOYPAD_PORT_COUNT] = {0};
+
+ timer_init();
+ joypad_init();
+ debug_init_emulog();
+
+ console_init();
+ console_set_render_mode(RENDER_MANUAL);
+ console_set_debug(false);
+
+ bio_sensor_init();
+
+ while (1)
+ {
+ console_clear();
+ joypad_poll();
+
+ printf("LibDragon Bio Sensor Subsystem Test ROM\n\n");
+ printf("Connect up to 4 controllers with Bio Sensor accessories\n");
+ printf("\n");
+ printf("Press A to start reading the Bio Sensor\n");
+ printf("Press B to stop reading the Bio Sensor\n");
+ printf("\n");
+
+ JOYPAD_PORT_FOREACH (port)
+ {
+ pressed = joypad_get_buttons_pressed(port);
+
+ // Update pulse history
+ if (bio_sensor_get_active(port))
+ {
+ printf("Port %d: ", port + 1);
+ printf("BPM: %03d ", bio_sensor_get_bpm(port));
+
+ history[port].buffer[history[port].index] = bio_sensor_get_pulsing(port);
+ history[port].index = (history[port].index + 1) % HISTOGRAM_WIDTH;
+
+ if (pressed.b)
+ {
+ bio_sensor_read_stop(port);
+ // Clear history when stopping
+ memset(&history[port], 0, sizeof(pulse_history_t));
+ }
+ printf("Active");
+ }
+ else if (joypad_get_accessory_type(port) == JOYPAD_ACCESSORY_TYPE_BIO_SENSOR)
+ {
+ printf("Port %d: BPM: --- ", port + 1);
+ if (pressed.a)
+ {
+ bio_sensor_read_start(port);
+ }
+ printf("Stopped; Press A to start");
+ }
+ else
+ {
+ printf("Port %d: BPM: --- ", port + 1);
+ printf("Bio Sensor not connected");
+ }
+ printf("\n");
+
+ // Draw histogram
+ printf("[");
+ for (int i = HISTOGRAM_WIDTH - 1; i >- 0; i--)
+ {
+ int idx = (history[port].index + i) % HISTOGRAM_WIDTH;
+ if (bio_sensor_get_active(port))
+ {
+ printf("%c", history[port].buffer[idx] ? '^' : '_');
+ }
+ else
+ {
+ printf("_");
+ }
+ }
+ printf("]\n\n");
+ }
+
+ console_render();
+ }
+}
diff --git a/examples/brew-volley/.gitignore b/examples/brew-volley/.gitignore
new file mode 100644
index 000000000..f5802895b
--- /dev/null
+++ b/examples/brew-volley/.gitignore
@@ -0,0 +1,4 @@
+build/
+filesystem/
+*.z64
+*.z64
diff --git a/examples/brew-volley/Makefile b/examples/brew-volley/Makefile
new file mode 100644
index 000000000..d3b055f58
--- /dev/null
+++ b/examples/brew-volley/Makefile
@@ -0,0 +1,61 @@
+TARGET=brew_volley
+BUILD_DIR=build
+FSYSTEM_DIR=filesystem
+# This example uses mixer_try_play, which is a preview API
+LIBDRAGON_PREVIEW = 2
+include $(N64_INST)/include/n64.mk
+
+src = main.c
+assets_xm = $(wildcard assets/*.xm)
+assets_wav = $(wildcard assets/*.wav)
+assets_png = $(wildcard assets/*.png)
+assets_ttf = $(wildcard assets/*.ttf)
+
+assets_conv = $(addprefix filesystem/,$(notdir $(assets_xm:%.xm=%.xm64))) \
+ $(addprefix filesystem/,$(notdir $(assets_wav:%.wav=%.wav64))) \
+ $(addprefix filesystem/,$(notdir $(assets_ttf:%.ttf=%.font64))) \
+ $(addprefix filesystem/,$(notdir $(assets_png:%.png=%.sprite)))
+
+#N64_CFLAGS = -Wno-error
+AUDIOCONV_FLAGS ?=
+MKSPRITE_FLAGS ?=
+
+all: $(TARGET).z64
+
+filesystem/%.xm64: assets/%.xm
+ @mkdir -p $(dir $@)
+ @echo " [AUDIO] $@"
+ @$(N64_AUDIOCONV) $(AUDIOCONV_FLAGS) -o filesystem $<
+
+filesystem/%.wav64: assets/%.wav
+ @mkdir -p $(dir $@)
+ @echo " [AUDIO] $@"
+ @$(N64_AUDIOCONV) --wav-compress 1 -o filesystem $<
+
+filesystem/%.sprite: assets/%.png
+ @mkdir -p $(dir $@)
+ @echo " [SPRITE] $@"
+ @$(N64_MKSPRITE) --format RGBA32 $(MKSPRITE_FLAGS) -o filesystem "$<"
+
+filesystem/%.font64: assets/%.ttf
+ @mkdir -p $(dir $@)
+ @echo " [FONT] $@"
+ @$(N64_MKFONT) $(MKFONT_FLAGS) -o filesystem "$<"
+
+filesystem/n64brew.sprite: MKSPRITE_FLAGS=--format RGBA32 --tiles 32,32
+filesystem/background.sprite: MKSPRITE_FLAGS=--format RGBA16 --tiles 32,32
+filesystem/Pacifico.font64: MKFONT_FLAGS+=--size 32
+
+$(BUILD_DIR)/$(TARGET).dfs: $(assets_conv)
+$(BUILD_DIR)/$(TARGET).elf: $(src:%.c=$(BUILD_DIR)/%.o)
+
+$(TARGET).z64: N64_ROM_TITLE="N64brew GameJam 4"
+$(TARGET).z64: N64_ROM_METADATA=metadata/metadata.ini
+$(TARGET).z64: $(BUILD_DIR)/$(TARGET).dfs
+
+clean:
+ rm -rf $(BUILD_DIR) $(FSYSTEM_DIR) $(TARGET).z64
+
+-include $(wildcard $(BUILD_DIR)/*.d)
+
+.PHONY: all clean
diff --git a/examples/brew-volley/README.md b/examples/brew-volley/README.md
new file mode 100644
index 000000000..e39d943d0
--- /dev/null
+++ b/examples/brew-volley/README.md
@@ -0,0 +1,17 @@
+# Brew Volley
+
+This is a modified MULLER Marian's (tfmoe__) entry for the N64brew Game Jam #4 (a.k.a N64brew Summer Game Jam). Released as a Libdragon example with the permission from the author.
+
+The game should be played with two controllers.
+
+This example shows how one can easily build a simple game with unstable Libdragon SDK using RDPQ library.
+
+# Assets attributions
+
+Music: [Autang](https://modarchive.org/index.php?request=view_by_moduleid&query=167849) by JAM is licensed under Public Domain.
+
+Background: ["Coconut tree near shore within mountain range" by Elizeu Dias](https://unsplash.com/photos/RN6ts8IZ4_0) Licensed under Unsplash license.
+
+Code is licensed under Unlicense.
+
+Everything else in this example is licensed under CC0.
diff --git a/examples/brew-volley/assets/Pacifico.ttf b/examples/brew-volley/assets/Pacifico.ttf
new file mode 100644
index 000000000..e7def95d3
Binary files /dev/null and b/examples/brew-volley/assets/Pacifico.ttf differ
diff --git a/examples/brew-volley/assets/autang.xm b/examples/brew-volley/assets/autang.xm
new file mode 100644
index 000000000..bf1e74ae9
Binary files /dev/null and b/examples/brew-volley/assets/autang.xm differ
diff --git a/examples/brew-volley/assets/background.png b/examples/brew-volley/assets/background.png
new file mode 100644
index 000000000..39b6a60fd
Binary files /dev/null and b/examples/brew-volley/assets/background.png differ
diff --git a/examples/brew-volley/assets/ball.png b/examples/brew-volley/assets/ball.png
new file mode 100644
index 000000000..c461301b3
Binary files /dev/null and b/examples/brew-volley/assets/ball.png differ
diff --git a/examples/brew-volley/assets/halt.wav b/examples/brew-volley/assets/halt.wav
new file mode 100644
index 000000000..e6f22253e
Binary files /dev/null and b/examples/brew-volley/assets/halt.wav differ
diff --git a/examples/brew-volley/assets/hit.wav b/examples/brew-volley/assets/hit.wav
new file mode 100644
index 000000000..121316f99
Binary files /dev/null and b/examples/brew-volley/assets/hit.wav differ
diff --git a/examples/brew-volley/assets/n64brew.png b/examples/brew-volley/assets/n64brew.png
new file mode 100644
index 000000000..106eb6b4a
Binary files /dev/null and b/examples/brew-volley/assets/n64brew.png differ
diff --git a/examples/brew-volley/assets/net.png b/examples/brew-volley/assets/net.png
new file mode 100644
index 000000000..17e7ea625
Binary files /dev/null and b/examples/brew-volley/assets/net.png differ
diff --git a/examples/brew-volley/assets/win.wav b/examples/brew-volley/assets/win.wav
new file mode 100644
index 000000000..0c6643dc4
Binary files /dev/null and b/examples/brew-volley/assets/win.wav differ
diff --git a/examples/brew-volley/main.c b/examples/brew-volley/main.c
new file mode 100644
index 000000000..da9835f3b
--- /dev/null
+++ b/examples/brew-volley/main.c
@@ -0,0 +1,555 @@
+#include
+#include
+
+static sprite_t *background_sprite;
+static sprite_t *brew_sprite;
+static sprite_t *ball_sprite;
+static sprite_t *net_sprite;
+static rspq_block_t* background_block;
+
+static xm64player_t sfx_music;
+static wav64_t sfx_hit;
+static wav64_t sfx_halt;
+static wav64_t sfx_win;
+
+enum {
+ FONT_PACIFICO = 1,
+};
+
+typedef struct {
+ float x;
+ float y;
+} vector2d_t;
+
+typedef struct {
+ vector2d_t pos;
+ vector2d_t dir;
+ vector2d_t normalized;
+ float length;
+} collision_t;
+
+typedef struct {
+ float x;
+ float y;
+ float dx;
+ float dy;
+ float scale_factor;
+} object_t;
+
+#define NUM_BLOBS 2
+#define INITIAL_COUNTDOWN 3
+#define MAX_POINTS 21
+
+#define FRAMERATE 60
+#define AIR_FRICTION_FACTOR 0.99f
+#define GROUND_FRICTION_FACTOR 0.9f
+#define GRAVITY_FACTOR 9.81f
+#define SPEED_EPSILON 1e-1
+#define POSITION_EPSILON 10
+
+static object_t blobs[NUM_BLOBS];
+static object_t ball;
+static object_t net;
+
+static int32_t obj_min_x;
+static int32_t obj_max_x;
+static int32_t obj_min_y;
+static int32_t obj_max_y;
+static int32_t cur_tick = 0;
+
+// DEBUG collisions
+static collision_t collisions[NUM_BLOBS];
+
+static int scorePlayer1 = 0;
+static int scorePlayer2 = 0;
+static int lastPlayer = -1;
+static int hitCount = 0;
+static int countdown = 0;
+uint64_t startTime = 0;
+
+// Mixer channel allocation
+#define CHANNEL_SFX1 0
+#define CHANNEL_SFX2 2
+#define CHANNEL_SFX3 4
+#define CHANNEL_MUSIC 6
+
+
+void init_player(uint32_t i) {
+ uint32_t display_width = display_get_width();
+ object_t* obj = &blobs[i];
+ obj->x = i == 0 ? 40 : display_width - brew_sprite->width - 40;
+ obj->y = obj_max_y - brew_sprite->height;
+ obj->dx = 0;
+ obj->dy = 0;
+ obj->scale_factor = 1.0f;
+}
+
+bool rectRect(float r1x, float r1y, float r1w, float r1h, float r2x, float r2y, float r2w, float r2h) {
+ return (r1x + r1w >= r2x && // r1 right edge past r2 left
+ r1x <= r2x + r2w && // r1 left edge past r2 right
+ r1y + r1h >= r2y && // r1 top edge past r2 bottom
+ r1y <= r2y + r2h); // r1 bottom edge past r2 top
+}
+
+collision_t circleRect(float cx, float cy, float radius, float rx, float ry, float rw, float rh) {
+ float nearestX = cx;
+ float nearestY = cy;
+
+ // which edge is closest?
+ if (cx < rx) nearestX = rx; // test left edge
+ else if (cx > rx+rw) nearestX = rx+rw; // right edge
+ if (cy < ry) nearestY = ry; // top edge
+ else if (cy > ry+rh) nearestY = ry+rh; // bottom edge
+
+ // get distance from closest edges
+ float distX = cx - nearestX;
+ float distY = cy - nearestY;
+ float distance = sqrt( (distX*distX) + (distY*distY) );
+
+ // if the distance is less than the radius, collision!
+ vector2d_t pos = {nearestX, nearestY};
+ vector2d_t dir = {distX, distY};
+ vector2d_t normal = {0, 0};
+ if (distance > 0 && distance <= radius) {
+ normal.x = distX/distance;
+ normal.y = distY/distance;
+ }
+ collision_t retval = {pos, dir, normal, distance};
+ return retval;
+}
+
+void applyScreenLimits(float x, float y, float w, float h, float dx, float dy, object_t* obj) {
+ float next_x = x + dx;
+ float next_y = y + dy;
+
+ if (next_x + w >= obj_max_x) {
+ next_x = obj_max_x - (next_x + w - obj_max_x) - w;
+ obj->dx = -1.0 * dx;
+ }
+ if (next_x < obj_min_x) {
+ next_x = obj_min_x + (obj_min_x - next_x);
+ obj->dx = -1.0 * dx;
+ }
+ if (next_y + h >= obj_max_y) {
+ next_y = obj_max_y - (next_y + h - obj_max_y) - h + 1;
+ obj->dy = -1.0 * dy / 2;
+ }
+ if (next_y < obj_min_y) {
+ next_y = obj_min_y + (obj_min_x - next_y);
+ obj->dy = -1.0 * dy;
+ }
+
+ obj->x = next_x;
+ obj->y = next_y;
+}
+
+void applyScreenLimitsRect(object_t* obj, sprite_t* sprite) {
+ applyScreenLimits(obj->x, obj->y, sprite->width, sprite->height, obj->dx, obj->dy, obj);
+}
+
+void applyScreenLimitsCircle(object_t* obj, sprite_t* sprite) {
+ applyScreenLimits(obj->x - sprite->width/2, obj->y - sprite->height/2, sprite->width, sprite->height, obj->dx, obj->dy, obj);
+ obj->x += sprite->width/2;
+ obj->y += sprite->height/2;
+}
+
+void applyFriction(object_t* obj) {
+ if (obj->dx != 0) {
+ if (fabs(obj->dx) < SPEED_EPSILON) {
+ obj->dx = 0;
+ } else {
+ float factor = (obj->y < obj_max_y) ? AIR_FRICTION_FACTOR : GROUND_FRICTION_FACTOR;
+ float next_dx = fabs(obj->dx) * factor;
+ if (obj->dx < 0) {
+ obj->dx = -1.0f * next_dx;
+ } else {
+ obj->dx = next_dx;
+ }
+ }
+ }
+}
+
+void applyGravity(object_t* obj) {
+ if (obj->dy > 0 && obj->dy < SPEED_EPSILON && (obj_max_y - fabs(obj->y)) < POSITION_EPSILON) {
+ obj->dy = 0;
+ obj->y = obj_max_y;
+ } else if (obj->y < obj_max_y - ball_sprite->height) {
+ float next_dy = obj->dy + (GRAVITY_FACTOR / FRAMERATE);
+ obj->dy = next_dy;
+ }
+}
+
+int get_winner() {
+ return (scorePlayer1 >= MAX_POINTS && (scorePlayer1 - scorePlayer2) > 1)
+ ? 1
+ : (scorePlayer2 >= MAX_POINTS && (scorePlayer2 - scorePlayer1) > 1)
+ ? 2
+ : 0;
+}
+
+bool in_play() {
+ return countdown == 0 && !get_winner();
+}
+
+void update(int ovfl)
+{
+ if (!in_play()) {
+ uint64_t now = timer_ticks() / (TICKS_PER_SECOND / 1000);
+ uint64_t elapsed = now < startTime ? (now + (91625 - startTime)) : (now - startTime);
+ countdown = INITIAL_COUNTDOWN - (elapsed / 1000);
+ // New game
+ if (countdown == 0 && !in_play()) {
+ scorePlayer1 = 0;
+ scorePlayer2 = 0;
+ countdown = INITIAL_COUNTDOWN;
+ startTime = timer_ticks() / (TICKS_PER_SECOND / 1000);
+ }
+ return;
+ }
+
+ // Ball
+ // Ball hits ground ???
+ if (ball.y + ball.dy + ball_sprite->height/2 >= obj_max_y) {
+ // Sound FX
+ wav64_play(&sfx_halt, CHANNEL_SFX2);
+ uint32_t display_width = display_get_width();
+ // score + no more hits
+ if (ball.x > net.x) {
+ scorePlayer1++;
+ ball.x = display_width / 4.0f;
+ } else {
+ scorePlayer2++;
+ ball.x = 3.0 * (display_width / 4.0f);
+ }
+ ball.y = obj_min_y + ball_sprite->height/2;
+ ball.dx = 0;
+ ball.dy = 0;
+ hitCount = 0;
+ lastPlayer = -1;
+ // relocate players
+ for (uint32_t i = 0; i < NUM_BLOBS; i++) {
+ init_player(i);
+ }
+ // Handle next point (with a little pause)
+ countdown = INITIAL_COUNTDOWN; // Don't allow moves during countdown
+ startTime = get_ticks_ms();
+ // Handle end of game
+ int winner = get_winner();
+ if (winner) {
+ // play sfx
+ wav64_play(&sfx_win, CHANNEL_SFX3);
+ }
+ }
+ applyScreenLimitsCircle(&ball, ball_sprite);
+ // air friction + gravity
+ applyFriction(&ball);
+ applyGravity(&ball);
+
+ // Handle collision with net
+ collision_t netCollision = circleRect(ball.x, ball.y, ball_sprite->width/2, net.x, net.y, net_sprite->width, net_sprite->height);
+ vector2d_t netCollisionNormal = netCollision.normalized;
+ if (netCollisionNormal.x != 0 || netCollisionNormal.y != 0) {
+ // Stop / bounce
+ // Use (normalized) vector from player center to ball center instead of nearest collision poiint ???
+ float distX = ball.x - (net.x + net_sprite->width/2);
+ float distY = ball.y - (net.y + net_sprite->height/2);
+ float distance = sqrt( (distX*distX) + (distY*distY) );
+ vector2d_t netBallNormal = {
+ distX/distance,
+ distY/distance
+ };
+ netCollisionNormal = netBallNormal;
+
+ // if hitting on the side, reverse ball dx
+ // If hitting on top, reverse ball dy
+ float next_ball_dx = (netCollision.pos.x == net.x || netCollision.pos.x == (net.x + net_sprite->width)) ? -1.0f * ball.dx : ball.dx;
+ float next_ball_dy = (netCollision.pos.y == net.y) ? -1.0f *ball.dy : ball.dy;
+ ball.dx = next_ball_dx;
+ ball.dy = next_ball_dy;
+
+ // Resolve collisions --> move ball
+ float next_ball_x = ball.x;
+ float next_ball_y = ball.y;
+ // dependns on nearest X/Y if to the right, add x, if to the left, sub x, if to the top, add y if to the bottom, sub y
+ if (netCollision.pos.x == net.x) {
+ // Ball is on the left
+ next_ball_x -= ball_sprite->width/2 - fabs(netCollision.dir.x);
+ } else if (netCollision.pos.x == (net.x + net_sprite->width)) {
+ // Ball is on the right
+ next_ball_x += ball_sprite->width/2 - fabs(netCollision.dir.x);
+ } else if (netCollision.pos.y == net.y) {
+ // Ball is on the top
+ next_ball_y -= ball_sprite->height/2 - fabs(netCollision.dir.y);
+ } else if (netCollision.pos.y == (net.y + net_sprite->height)) {
+ // Ball is on the bottom
+ next_ball_y += ball_sprite->height/2 - fabs(netCollision.dir.y);
+ }
+
+ ball.x = next_ball_x;
+ ball.y = next_ball_y;
+ }
+
+ for (uint32_t i = 0; i < NUM_BLOBS; i++)
+ {
+ object_t *obj = &blobs[i];
+ applyScreenLimitsRect(obj, brew_sprite); // Handle with collisions to be resolved all at once
+ // Apply gravity / friction
+ applyFriction(obj);
+ applyGravity(obj);
+
+ // Handle collisions
+ // Player / Net (bounce / block)
+ // Player / Ball (up to 3 hits per turn)
+ // Screen borders / Ball (bounce, loose some momentum)
+ // Ground / Ball (end point)
+ // Player / Bonus ??? (higher bounce, faster speed, move net down/up, ...)
+
+ // Player/net collision
+ if (rectRect(obj->x, obj->y, brew_sprite->width, brew_sprite->height, net.x, net.y, net_sprite->width, net_sprite->height)) {
+ // Reposition player to the left/right of net
+ if (obj->x < net.x) {
+ obj->x = net.x - brew_sprite->width;
+ } else {
+ obj->x = net.x + net_sprite->width;
+ }
+ }
+
+ // Ball collision
+ collision_t collision = circleRect(ball.x, ball.y, ball_sprite->width/2, obj->x, obj->y, brew_sprite->width, brew_sprite->height);
+ vector2d_t collisionNormal = collision.normalized;
+ if ((collisionNormal.x != 0 || collisionNormal.y != 0) && !(lastPlayer == i && hitCount > 2)) {
+ // Use (normalized) vector from player center to ball center instead of nearest collision poiint ???
+ float distX = ball.x - (obj->x + brew_sprite->width/2);
+ float distY = ball.y - (obj->y + brew_sprite->height/2);
+ float distance = sqrt( (distX*distX) + (distY*distY) );
+ vector2d_t playerBallNormal = {
+ distX/distance,
+ distY/distance
+ };
+ collisionNormal = playerBallNormal;
+
+ // should bounce even if obj is not moving !!!
+ // should depend on the ball position relative to the player ??
+ float next_ball_dx = obj->dx - ball.dx;
+ float next_ball_dy = obj->dy - ball.dy;
+
+ // bounce with ball velocity
+ ball.dx = next_ball_dx;
+ ball.dy = next_ball_dy;
+
+ // Resolve collisions --> move ball
+ float next_ball_x = ball.x;
+ float next_ball_y = ball.y;
+ // dependns on nearest X/Y if to the right, add x, if to the left, sub x, if to the top, add y if to the bottom, sub y
+ if (collision.pos.x == obj->x) {
+ // Ball is on the left
+ next_ball_x -= ball_sprite->width/2 - fabs(collision.dir.x);
+ } else if (collision.pos.x == (obj->x + brew_sprite->width)) {
+ // Ball is on the right
+ next_ball_x += ball_sprite->width/2 - fabs(collision.dir.x);
+ } else if (collision.pos.y == obj->y) {
+ // Ball is on the top
+ next_ball_y -= ball_sprite->height/2 - fabs(collision.dir.y);
+ } else if (collision.pos.y == (obj->y + brew_sprite->height)) {
+ // Ball is on the bottom
+ next_ball_y += ball_sprite->height/2 - fabs(collision.dir.y);
+ }
+ ball.x = next_ball_x;
+ ball.y = next_ball_y;
+
+ // Max 3 hits per player
+ if (lastPlayer != i) {
+ lastPlayer = i;
+ hitCount = 0;
+ }
+ hitCount++;
+
+ // Sound FX
+ wav64_play(&sfx_hit, CHANNEL_SFX1);
+ }
+ collisions[i] = collision;
+ }
+
+ cur_tick++;
+}
+
+void render(int cur_frame)
+{
+ surface_t *disp = display_get();
+ rdpq_attach_clear(disp, NULL);
+
+ // Render a background with copy-mode and under an rspq block for optimization purposes
+ if(!background_block){
+ rspq_block_begin();
+
+ rdpq_set_mode_copy(false);
+ rdpq_sprite_blit(background_sprite, 0, 0, &(rdpq_blitparms_t){
+ .scale_x = 1, .scale_y = 1,
+ });
+
+ // Setup a mode for the rest of the sprites
+ rdpq_set_mode_standard();
+ rdpq_mode_filter(FILTER_BILINEAR);
+ rdpq_mode_alphacompare(1);
+ rdpq_mode_antialias(AA_NONE);
+ rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY);
+
+ background_block = rspq_block_end();
+ } rspq_block_run(background_block);
+
+ // Players
+ for (uint32_t i = 0; i < NUM_BLOBS; i++)
+ {
+ rdpq_sprite_blit(brew_sprite, blobs[i].x, (int32_t) blobs[i].y, &(rdpq_blitparms_t){
+ .scale_x = blobs[i].scale_factor, .scale_y = blobs[i].scale_factor,
+ });
+ }
+
+ // Ball
+ rdpq_sprite_blit(ball_sprite, (ball.x - ball_sprite->width/2), (int32_t) (ball.y - ball_sprite->height/2), &(rdpq_blitparms_t){
+ .scale_x = ball.scale_factor, .scale_y = ball.scale_factor,
+ });
+
+
+ // Draw net
+ rdpq_sprite_blit(net_sprite, net.x, net.y, &(rdpq_blitparms_t){
+ .scale_x = net.scale_factor, .scale_y = net.scale_factor,
+ });
+
+ // Draw text
+ rdpq_text_printf(&(rdpq_textparms_t){
+ .align = ALIGN_CENTER,
+ .valign = VALIGN_TOP,
+ .width = 200,
+ .height = 200,
+ .wrap = WRAP_WORD,
+ }, FONT_PACIFICO, 210, 20, "^00Score:\n %d | %d", scorePlayer1, scorePlayer2);
+
+ int winner = get_winner();
+ if (winner) {
+ rdpq_text_printf(&(rdpq_textparms_t){
+ .align = ALIGN_CENTER,
+ .width = 400,
+ }, FONT_PACIFICO, 120, 180, "^01Player %d WINS!", winner);
+ } else {
+ if (countdown > 0) {
+ rdpq_text_printf(&(rdpq_textparms_t){
+ .align = ALIGN_CENTER,
+ .width = 400,
+ }, FONT_PACIFICO, 120, 180, "^01%d", countdown);
+ }
+ }
+
+ // Force backbuffer flip
+ rdpq_detach_show();
+}
+
+#include
+#include "n64sys.h"
+
+int main()
+{
+ debug_init_emulog();
+ debug_init_usblog();
+
+ display_init(RESOLUTION_640x480, DEPTH_16_BPP, 3, GAMMA_NONE, FILTERS_DEDITHER);
+
+ joypad_init();
+ timer_init();
+
+ uint32_t display_width = display_get_width();
+ uint32_t display_height = display_get_height();
+
+ dfs_init(DFS_DEFAULT_LOCATION);
+
+ rdpq_init();
+
+ audio_init(44100, 4);
+ mixer_init(20);
+
+ wav64_open(&sfx_hit, "rom:/hit.wav64");
+ wav64_open(&sfx_halt, "rom:/halt.wav64");
+ wav64_open(&sfx_win, "rom:/win.wav64");
+
+ xm64player_open(&sfx_music, "rom:/autang.xm64");
+ xm64player_set_loop(&sfx_music, true);
+ xm64player_set_vol(&sfx_music, 0.55);
+ xm64player_play(&sfx_music, CHANNEL_MUSIC);
+
+ background_sprite = sprite_load("rom:/background.sprite");
+
+ brew_sprite = sprite_load("rom:/n64brew.sprite");
+
+ rdpq_font_t *fnt1 = rdpq_font_load("rom:/Pacifico.font64");
+ rdpq_font_style(fnt1, 0, &(rdpq_fontstyle_t){
+ .color = RGBA32(0xFD, 0xFE, 0x99, 0xFF),
+ });
+ rdpq_font_style(fnt1, 1, &(rdpq_fontstyle_t){
+ .color = RGBA32(0xFD, 0x9E, 0x99, 0xFF),
+ });
+ rdpq_text_register_font(FONT_PACIFICO, fnt1);
+
+ obj_min_x = 5;
+ obj_max_x = display_width - 5;
+ obj_min_y = 5;
+ obj_max_y = display_height - 16;
+
+ for (uint32_t i = 0; i < NUM_BLOBS; i++)
+ {
+ init_player(i);
+ }
+
+
+ ball_sprite = sprite_load("rom:/ball.sprite");
+ ball.x = display_width / 4.0f;
+ ball.y = obj_min_y + ball_sprite->height/2;
+ ball.dx = 0;
+ ball.dy = 0;
+ ball.scale_factor = 1.0f;
+
+ net_sprite = sprite_load("rom:/net.sprite");
+ net.x = display_width/2.0f - (net_sprite->width/2.0f);
+ net.y = display_height - net_sprite->height;
+ net.dx = 0;
+ net.dy = 0;
+ net.scale_factor = 1.0f;
+
+ countdown = INITIAL_COUNTDOWN;
+ startTime = get_ticks_ms();
+
+ int cur_frame = 0;
+ while (1)
+ {
+ update(0);
+ render(cur_frame);
+ joypad_poll();
+
+ if (in_play()) {
+ for (uint32_t i = 0; i < NUM_BLOBS; i++)
+ {
+ if (joypad_is_connected((joypad_port_t) i)) {
+ joypad_buttons_t buttons = joypad_get_buttons((joypad_port_t) i);
+ object_t *obj = &blobs[i];
+
+ if ((buttons.d_up || buttons.a || buttons.b) && (obj_max_y - fabs(obj->y) - brew_sprite->height) < POSITION_EPSILON) {
+ obj->dy = -6;
+ }
+
+ if (buttons.d_left) {
+ obj->dx = -6;
+ }
+
+ if (buttons.d_right) {
+ obj->dx = 6;
+ }
+ }
+ }
+ }
+
+ // Check whether one audio buffer is ready, otherwise wait for next
+ // frame to perform mixing.
+ mixer_try_play();
+
+ cur_frame++;
+ }
+}
diff --git a/examples/brew-volley/metadata/boxart.png b/examples/brew-volley/metadata/boxart.png
new file mode 100644
index 000000000..11a9ffddd
Binary files /dev/null and b/examples/brew-volley/metadata/boxart.png differ
diff --git a/examples/brew-volley/metadata/metadata.ini b/examples/brew-volley/metadata/metadata.ini
new file mode 100644
index 000000000..b361284d9
--- /dev/null
+++ b/examples/brew-volley/metadata/metadata.ini
@@ -0,0 +1,81 @@
+; Main metadata, in English
+[meta]
+; Name of the game (similar to the header name,
+; but encoded as UTF-8, so more flexible)
+name = Brew Volley
+
+; Author of the game (publisher, developer, etc.)
+author = Libdragon Team
+
+; Release date of the game. Use YYYY-MM-DD format.
+release-date = 2023-08-25
+
+; Open Source license the game is released under.
+; This must be the SPDX code of the license.
+; If the key is omitted or empty, it is assumed to
+; be a non open-source game
+osi-license = Unlicense
+
+; URL of a website for a game. Can be a proper website
+; or just a GitHub repo
+website = https://github.com/n64brew/libdragon
+
+; Self-certified age rating for the game. This is a number
+; identifying the minimum suggested age for a player (0-18).
+; This allows readers to provide some kind of content filtering
+; capability. You can follow ESBR/PEGI guidelines to get
+; some suggestions on this.
+age-rating = 0
+
+; Maximum number of simultaneous players that the game supports.
+num-players = 1
+
+; Short description of the game, to be displayed in
+; menu or ROM managers. Suggested maximum length is
+; 120 chars.
+short-desc = Simple game to demonstrate basic rdpq usage
+
+; Long description of the game, stored in an external
+; file, to be displayed in menu or ROM managers.
+; The file must have a ".txt" extension and must contain
+; raw UTF-8 text with no specific markup. Assume it will
+; be word-wrapped during display, but newlines will be
+; preserved, so keep paragraphs in long lines.
+;long-desc = description.txt
+
+; Screenshots. If defined, this must be a list of
+; comma-separated files in the ZIP that contain
+; screenshots of the gameplay.
+; Supported formats are PNG or JPG
+; Screenshots are expected to be in native
+; resolution (eg: 320x240) or smaller.
+screenshots = volley1.png
+
+; Boxarts. All the keys in this section must be
+; associated with values that are filenames in the
+; metadata ZIP.
+; Not all keys must be specified, leave the others
+; commented.
+; Supported file formats are PNG and JPG.
+; Maximum resolution is 320x240 (or 240x320 for
+; vertical pictures).
+[boxart]
+front = boxart.png
+;back = bx_back.jpg
+;top =
+;bottom =
+;left =
+;right =
+
+; Cartarts (images of the stickers on the cartridge).
+; All the keys in this section must be
+; associated with values that are filenames in the
+; metadata ZIP.
+; Not all keys must be specified, leave the others
+; commented.
+; Supported file formats are PNG and JPG.
+; Maximum resolution is 320x240 (or 240x320 for
+; vertical pictures).
+[cartart]
+;front = cx_front.png
+;back = cx_back.png
diff --git a/examples/brew-volley/metadata/volley1.png b/examples/brew-volley/metadata/volley1.png
new file mode 100644
index 000000000..7d2208a0d
Binary files /dev/null and b/examples/brew-volley/metadata/volley1.png differ
diff --git a/examples/compression/Makefile b/examples/compression/Makefile
new file mode 100644
index 000000000..d220d2da1
--- /dev/null
+++ b/examples/compression/Makefile
@@ -0,0 +1,52 @@
+BUILD_DIR=build
+include $(N64_INST)/include/n64.mk
+
+src = compression.c
+assets = $(wildcard assets/*)
+
+assets_conv = $(addprefix filesystem/,$(notdir $(assets:%=%.c0))) \
+ $(addprefix filesystem/,$(notdir $(assets:%=%.c1))) \
+ $(addprefix filesystem/,$(notdir $(assets:%=%.c2))) \
+ $(addprefix filesystem/,$(notdir $(assets:%=%.c3)))
+
+AUDIOCONV_FLAGS ?=
+MKSPRITE_FLAGS ?=
+
+all: compression.z64
+
+filesystem/%.c0: assets/%
+ @mkdir -p $(dir $@)/c0
+ @echo " [MKASSET] $@"
+ $(N64_BINDIR)/mkasset -c 0 -w 256 -o filesystem/c0 $<
+ mv "filesystem/c0/$(notdir $<)" "$@"
+
+filesystem/%.c1: assets/%
+ @mkdir -p $(dir $@)/c1
+ @echo " [MKASSET] $@"
+ $(N64_BINDIR)/mkasset -c 1 -w 256 -o filesystem/c1 $<
+ mv "filesystem/c1/$(notdir $<)" "$@"
+
+filesystem/%.c2: assets/%
+ @mkdir -p $(dir $@)/c2
+ @echo " [MKASSET] $@"
+ $(N64_BINDIR)/mkasset -c 2 -w 256 -o filesystem/c2 $<
+ mv "filesystem/c2/$(notdir $<)" "$@"
+
+filesystem/%.c3: assets/%
+ @mkdir -p $(dir $@)/c3
+ @echo " [MKASSET] $@"
+ $(N64_BINDIR)/mkasset -c 3 -w 256 -o filesystem/c3 $<
+ mv "filesystem/c3/$(notdir $<)" "$@"
+
+$(BUILD_DIR)/compression.dfs: $(assets_conv)
+$(BUILD_DIR)/compression.elf: $(src:%.c=$(BUILD_DIR)/%.o)
+
+compression.z64: N64_ROM_TITLE="Compression Bench"
+compression.z64: $(BUILD_DIR)/compression.dfs
+
+clean:
+ rm -rf $(BUILD_DIR) compression.z64
+
+-include $(wildcard $(BUILD_DIR)/*.d)
+
+.PHONY: all clean
diff --git a/examples/compression/compression.c b/examples/compression/compression.c
new file mode 100644
index 000000000..a1e433b6b
--- /dev/null
+++ b/examples/compression/compression.c
@@ -0,0 +1,179 @@
+#include
+#include
+#include
+
+char *cmpfiles[1024];
+int num_files = 0;
+
+const uint32_t crc32_tab[] = {
+ 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f,
+ 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
+ 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2,
+ 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
+ 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9,
+ 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
+ 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,
+ 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
+ 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423,
+ 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
+ 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106,
+ 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
+ 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d,
+ 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
+ 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950,
+ 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
+ 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7,
+ 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
+ 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa,
+ 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
+ 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
+ 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
+ 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84,
+ 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
+ 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb,
+ 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
+ 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e,
+ 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
+ 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55,
+ 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
+ 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28,
+ 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
+ 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f,
+ 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
+ 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
+ 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
+ 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69,
+ 0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
+ 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc,
+ 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
+ 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693,
+ 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
+ 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
+};
+
+uint32_t crc32(const void *buf, size_t size)
+{
+ const uint8_t *p = buf;
+ uint32_t crc;
+
+ crc = ~0U;
+ while (size--)
+ crc = crc32_tab[(crc ^ *p++) & 0xFF] ^ (crc >> 8);
+ return crc ^ ~0U;
+}
+
+static bool strendswith(const char *str, const char *suffix) {
+ char *p = strstr(str, suffix);
+ return p && p[strlen(suffix)] == '\0';
+}
+
+typedef struct {
+ const char *fn;
+ int size;
+ uint32_t full_crc;
+ uint32_t partial_crc;
+ uint32_t full_us;
+ uint32_t partial_us;
+} benchmark;
+
+benchmark run_bench(const char *fn) {
+
+ int sz1;
+ // debugf("FULL\n");
+ disable_interrupts();
+ uint32_t t0s = get_ticks();
+ void *buf1 = asset_load(fn, &sz1);
+ uint32_t t0e = get_ticks();
+ enable_interrupts();
+ uint32_t crc1 = crc32(buf1, sz1);
+ // debugf("CRC %08lx %d\n", crc1, sz1);
+
+ // debug_hexdump(buf1, sz1);
+
+ // debugf("PARTIAL\n");
+ disable_interrupts();
+ int sz;
+ FILE *f = asset_fopen(fn, &sz);
+ void *buf2 = malloc(sz);
+ uint32_t t1s = get_ticks();
+ int sz2 = fread(buf2, 1, sz*2, f);
+ uint32_t t1e = get_ticks();
+ enable_interrupts();
+ assertf(sz == sz2, "fread %d != %d", sz, sz2);
+ uint32_t crc2 = crc32(buf2, sz);
+ // debugf("CRC %08lx\n", crc2);
+
+ fclose(f);
+ free(buf1);
+ free(buf2);
+
+ return (benchmark){
+ .fn = fn,
+ .size = sz,
+ .full_crc = crc1,
+ .partial_crc = crc2,
+ .full_us = TIMER_MICROS(t0e-t0s),
+ .partial_us = TIMER_MICROS(t1e-t1s),
+ };
+}
+
+int file_size(const char *fn) {
+ FILE *f = fopen(fn, "rb");
+ fseek(f, 0, SEEK_END);
+ int sz = ftell(f);
+ fclose(f);
+ return sz;
+}
+
+int main(void) {
+ debug_init_usblog();
+ debug_init_emulog();
+
+ console_init();
+ console_set_debug(true);
+ dfs_init(DFS_DEFAULT_LOCATION);
+ asset_init_compression(2);
+ asset_init_compression(3);
+
+ dir_t dff;
+ if (dir_findfirst("rom:/", &dff) == 0) {
+ do {
+ if (dff.d_type == DT_REG)
+ if (strendswith(dff.d_name, ".c0") || strendswith(dff.d_name, ".c1") ||
+ strendswith(dff.d_name, ".c2") || strendswith(dff.d_name, ".c3")) {
+ cmpfiles[num_files] = malloc(5 + strlen(dff.d_name) + 1);
+ sprintf(cmpfiles[num_files++], "rom:/%s", dff.d_name);
+ }
+ } while (dir_findnext("rom:/", &dff) == 0);
+ }
+
+ // Sort cmpfiles by name
+ int sort_name(const void *a, const void *b) {
+ return strcmp(*(char **)a, *(char **)b);
+ }
+ qsort(cmpfiles, num_files, sizeof(char *), sort_name);
+
+ printf("Decompression benchmark: %d files\n", num_files);
+ if (true) {
+ printf("%-28s: %-4s | %-7s | %-5s | %-5s\n", "File", "KiB", "Ratio", "Full", "Partial");
+ for (int i=0; i
+#include
+
+const char *format_accessory_type(int accessory_type)
+{
+ switch (accessory_type)
+ {
+ case ACCESSORY_NONE:
+ return "None ";
+ case ACCESSORY_MEMPAK:
+ return "Memory ";
+ case ACCESSORY_RUMBLEPAK:
+ return "Rumble Pak ";
+ case ACCESSORY_TRANSFERPAK:
+ return "Transfer Pak";
+ case ACCESSORY_VRU:
+ return "VRU ";
+ default:
+ return "Unknown ";
+ }
+}
+
+void print_inputs(_SI_condat *inputs)
+{
+ printf(
+ "Stick: %+04d,%+04d\n",
+ inputs->x, inputs->y
+ );
+ printf(
+ "D-U:%d D-D:%d D-L:%d D-R:%d C-U:%d C-D:%d C-L:%d C-R:%d\n",
+ inputs->up, inputs->down,
+ inputs->left, inputs->right,
+ inputs->C_up, inputs->C_down,
+ inputs->C_left, inputs->C_right
+ );
+ printf(
+ "A:%d B:%d L:%d R:%d Z:%d Start:%d\n",
+ inputs->A, inputs->B,
+ inputs->L, inputs->R,
+ inputs->Z, inputs->start
+ );
+}
+
+int main(void)
+{
+ timer_init();
+ controller_init();
+ debug_init_emulog();
+ console_init();
+ console_set_render_mode(RENDER_MANUAL);
+ console_set_debug(false);
+
+ while (1)
+ {
+ console_clear();
+
+ printf("LibDragon Controller Subsystem Test\n\n");
+
+ controller_scan();
+ struct controller_data inputs = get_keys_pressed();
+
+ for (int port = 0; port < 4; port++)
+ {
+ int accessory_type = identify_accessory(port);
+
+ printf("Port %d ", port + 1);
+ printf("Accessory: %s ", format_accessory_type(accessory_type));
+ printf("\n");
+ print_inputs(&inputs.c[port]);
+ printf("\n");
+ }
+
+ console_render();
+ }
+}
\ No newline at end of file
diff --git a/examples/coroutine/Makefile b/examples/coroutine/Makefile
new file mode 100644
index 000000000..7996d1b49
--- /dev/null
+++ b/examples/coroutine/Makefile
@@ -0,0 +1,20 @@
+BUILD_DIR=build
+SOURCE_DIR=.
+include $(N64_INST)/include/n64.mk
+
+N64_CXXFLAGS += -std=gnu++20 -fno-exceptions -Os
+
+all: coroutine.z64
+
+OBJS = $(BUILD_DIR)/coroutine.o
+
+$(BUILD_DIR)/coroutine.elf: $(OBJS)
+
+coroutine.z64: N64_ROM_TITLE="coroutine test"
+
+clean:
+ rm -rf $(BUILD_DIR) coroutine.z64
+
+-include $(wildcard $(BUILD_DIR)/*.d)
+
+.PHONY: all clean
diff --git a/examples/coroutine/coroutine.cpp b/examples/coroutine/coroutine.cpp
new file mode 100644
index 000000000..d7cee9477
--- /dev/null
+++ b/examples/coroutine/coroutine.cpp
@@ -0,0 +1,49 @@
+#include
+#include "coroutine.h"
+
+void worker(void* arg) {
+ u_int32_t a = 42;
+ u_int32_t b = 100;
+ float c = 4.0f;
+ float c2 = 4.0f;
+ for(int i=0; i<5; ++i)
+ {
+ printf("Hello from coroutine: %f %f\n", c, c2);
+ coro_yield();
+
+ printf("Hello from coroutine 2: %ld\n", a + b);
+ coro_yield();
+ ++a;
+ b *= 2;
+ c += 1;
+ c2 +=2;
+ }
+
+ coro_sleep(TICKS_FROM_MS(1000));
+}
+
+int main()
+{
+ debug_init_emulog();
+ debug_init_usblog();
+ console_init();
+ console_set_render_mode(RENDER_MANUAL);
+ float test = 1.0f;
+
+ auto co = coro_create(worker, NULL, 4096);
+
+ while (1) {
+ console_clear();
+
+ if(co)coro_resume(co);
+ wait_ms(100);
+
+ printf("Iteration: %f\n", test); test += 1;
+ printf("Done?: %d\n", coro_finished(co));
+ if(co && coro_finished(co)) {
+ coro_destroy(co);
+ co = nullptr;
+ }
+ console_render();
+ }
+}
diff --git a/examples/cpak-utest/Makefile b/examples/cpak-utest/Makefile
deleted file mode 100644
index 0f1f80082..000000000
--- a/examples/cpak-utest/Makefile
+++ /dev/null
@@ -1,17 +0,0 @@
-all: cpak-utest.z64
-.PHONY: all
-
-BUILD_DIR = build
-include $(N64_INST)/include/n64.mk
-
-OBJS = $(BUILD_DIR)/cpak-utest.o
-
-cpak-utest.z64: N64_ROM_TITLE = "CPak Unit Test"
-
-$(BUILD_DIR)/cpak-utest.elf: $(OBJS)
-
-clean:
- rm -rf $(BUILD_DIR) *.z64
-.PHONY: clean
-
--include $(wildcard $(BUILD_DIR)/*.d)
diff --git a/examples/cpak-utest/cpak-utest.c b/examples/cpak-utest/cpak-utest.c
deleted file mode 100644
index 1a95bf26d..000000000
--- a/examples/cpak-utest/cpak-utest.c
+++ /dev/null
@@ -1,300 +0,0 @@
-#include
-#include
-#include
-#include
-#include
-
-int main(void)
-{
- /* Initialize peripherals */
- console_init();
- joypad_init();
-
- console_set_render_mode(RENDER_MANUAL);
-
- console_clear();
-
- printf(
- "To test an inserted\n"
- "ControllerPak (mempak):\n\n"
- "Press A to validate Pak.\n\n"
- "Press B to format Pak.\n\n"
- "Press R to create entry.\n\n"
- "Press L to get entries.\n\n"
- "Press START to delete entry."
- );
-
- console_render();
-
- /* Main loop test */
- while (1)
- {
- /* To do initialize routines */
- joypad_poll();
-
- JOYPAD_PORT_FOREACH(port)
- {
- joypad_buttons_t keys = joypad_get_buttons_pressed(port);
-
- if (keys.a)
- {
- /* Make sure they don't have a Rumble Pak inserted instead */
- switch (joypad_get_accessory_type(port))
- {
- case JOYPAD_ACCESSORY_TYPE_NONE:
- printf("No accessory inserted!");
- break;
- case JOYPAD_ACCESSORY_TYPE_CONTROLLER_PAK:
- {
- int err;
- if ((err = validate_mempak(port)))
- {
- if (err == -3)
- {
- printf( "Controller Pak is not formatted!" );
- }
- else
- {
- printf( "Controller Pak bad or removed during read!" );
- }
- }
- else
- {
- for (int j = 0; j < 16; j++)
- {
- entry_structure_t entry;
-
- get_mempak_entry(port, j, &entry);
-
- if (entry.valid)
- {
- printf("%s - %d blocks\n", entry.name, entry.blocks);
- }
- else
- {
- printf("(EMPTY)\n");
- }
- }
-
- printf( "\nFree space: %d blocks", get_mempak_free_space(port ) );
- }
-
- break;
- }
- default:
- printf( "Cannot read data from this accessory!" );
- break;
- }
-
- console_render();
- }
- else if (keys.b)
- {
- console_clear();
-
- /* Make sure they don't have a Rumble Pak inserted instead */
- switch (joypad_get_accessory_type(port))
- {
- case JOYPAD_ACCESSORY_TYPE_NONE:
- printf("No accessory inserted!");
- break;
- case JOYPAD_ACCESSORY_TYPE_CONTROLLER_PAK:
- if (format_mempak(port))
- {
- printf( "Error formatting Controller Pak!" );
- }
- else
- {
- printf( "Controller Pak formatted!" );
- }
-
- break;
- default:
- printf("Cannot format this accessory!");
- break;
- }
-
- console_render();
- }
- else if (keys.l)
- {
- console_clear();
-
- /* Make sure they don't have a rumble pak inserted instead */
- switch (joypad_get_accessory_type(port))
- {
- case JOYPAD_ACCESSORY_TYPE_NONE:
- printf("No accessory inserted!");
- break;
- case JOYPAD_ACCESSORY_TYPE_CONTROLLER_PAK:
- {
- int err;
- if ((err = validate_mempak(port)))
- {
- if (err == -3)
- {
- printf( "Controller Pak is not formatted!" );
- }
- else
- {
- printf( "Controller Pak bad or removed during read!" );
- }
- }
- else
- {
- for (int j = 0; j < 16; j++ )
- {
- entry_structure_t entry;
-
- get_mempak_entry(port, j, &entry);
-
- if (entry.valid)
- {
- uint8_t *data = malloc( entry.blocks * MEMPAK_BLOCK_SIZE );
-
- printf( "Reading %s - %d blocks\n", entry.name, entry.blocks);
- printf( "Return: %d\n", read_mempak_entry_data(port, &entry, data));
-
- int new = 0;
- for (int k = 0; k < (12 * 12); k++)
- {
- printf( "%02X", data[k] );
-
- new++;
- if (new == 12)
- {
- printf( "\n" ); new = 0;
- }
- }
-
- free(data);
- break;
- }
- }
- }
-
- break;
- }
- default:
- printf("Cannot read data from this accessory!");
- break;
- }
-
- console_render();
- }
- else if (keys.r)
- {
- console_clear();
-
- /* Make sure they don't have a rumble pak inserted instead */
- switch (joypad_get_accessory_type(port))
- {
- case JOYPAD_ACCESSORY_TYPE_NONE:
- printf("No accessory inserted!");
- break;
- case JOYPAD_ACCESSORY_TYPE_CONTROLLER_PAK:
- {
- int err;
- if ((err = validate_mempak(port)))
- {
- if (err == -3)
- {
- printf( "Controller Pak is not formatted!" );
- }
- else
- {
- printf( "Controller Pak bad or removed during write!" );
- }
- }
- else
- {
- for (int j = 0; j < 16; j++)
- {
- entry_structure_t entry;
-
- get_mempak_entry(port, j, &entry);
-
- if (!entry.valid)
- {
- uint8_t *data = malloc( MEMPAK_BLOCK_SIZE );
- for (int k = 0; k < MEMPAK_BLOCK_SIZE; k++)
- {
- data[k] = k & 0xFF;
- }
-
- strcpy( entry.name, "TEST ENTRY.Z" );
- entry.blocks = 1;
- entry.region = 0x45;
-
- printf( "Writing %s - %d blocks\n", entry.name, entry.blocks);
- printf( "Return: %d\n", write_mempak_entry_data(port, &entry, data));
-
- free(data);
- break;
- }
- }
- }
-
- break;
- }
- default:
- printf("Cannot write data to this accessory!");
- break;
- }
-
- console_render();
- }
- else if (keys.start)
- {
- console_clear();
-
- /* Make sure they don't have a rumble pak inserted instead */
- switch (joypad_get_accessory_type(port))
- {
- case JOYPAD_ACCESSORY_TYPE_NONE:
- printf("No accessory inserted!");
- break;
- case JOYPAD_ACCESSORY_TYPE_CONTROLLER_PAK:
- {
- int err;
- if ((err = validate_mempak(port)))
- {
- if (err == -3)
- {
- printf( "Controller Pak is not formatted!" );
- }
- else
- {
- printf( "Controller Pak bad or removed during erase!" );
- }
- }
- else
- {
- for (int j = 0; j < 16; j++)
- {
- entry_structure_t entry;
-
- get_mempak_entry(port, j, &entry);
-
- if (entry.valid)
- {
- printf( "Deleting %s - %d blocks\n", entry.name, entry.blocks);
- printf( "Return: %d\n", delete_mempak_entry(port, &entry));
-
- break;
- }
- }
- }
-
- break;
- }
- default:
- printf("Cannot erase data from this accessory!");
- break;
- }
-
- console_render();
- }
- }
- }
-}
diff --git a/examples/cpakdemo/Makefile b/examples/cpakdemo/Makefile
new file mode 100644
index 000000000..371953f23
--- /dev/null
+++ b/examples/cpakdemo/Makefile
@@ -0,0 +1,21 @@
+all: cpakdemo.z64
+.PHONY: all
+
+BUILD_DIR = build
+# This example uses cpakfs, which is a preview API
+LIBDRAGON_PREVIEW = 2
+include $(N64_INST)/include/n64.mk
+N64_ROM_CONTROLLER1 = n64,pak=controller
+N64_ROM_REGIONFREE = true
+
+OBJS = $(BUILD_DIR)/cpakdemo.o
+
+cpakdemo.z64: N64_ROM_TITLE = "ControllerPak Demo"
+
+$(BUILD_DIR)/cpakdemo.elf: $(OBJS)
+
+clean:
+ rm -rf $(BUILD_DIR) *.z64
+.PHONY: clean
+
+-include $(wildcard $(BUILD_DIR)/*.d)
diff --git a/examples/cpakdemo/cpakdemo.c b/examples/cpakdemo/cpakdemo.c
new file mode 100644
index 000000000..b5899a9de
--- /dev/null
+++ b/examples/cpakdemo/cpakdemo.c
@@ -0,0 +1,622 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+// Buffer to save pak content for CopyToRAM/CopyFromRAM operation
+#define MAX_BANKS 62
+#define BANK_SIZE 32768
+static uint8_t pak_content[MAX_BANKS * BANK_SIZE];
+
+enum program_state_type {
+ STATE_PRINT_CONNECT_MSG,
+ STATE_WAIT_CONNECT,
+ STATE_PRINT_PAK_OVERVIEW,
+ STATE_WAIT_ACTION,
+ STATE_FSCK,
+ STATE_FSCK_FIX,
+ STATE_FORMAT,
+ STATE_FORMAT_ERASE,
+ STATE_CREATE_FILE,
+ STATE_DELETE_FILE,
+ STATE_COPY_TO_RAM,
+ STATE_COPY_FROM_RAM,
+ STATE_CORRUPT
+};
+
+void print_working_controller_message(joypad_port_t port) {
+ printf("Working with controller %d\n", port + 1);
+}
+
+void print_actions() {
+ printf("Start:List A:Check B:CreateFile R:CopyToRAM\n"
+ "Z+A:Fix Z+B:DeleteFile Z+R:CopyFromRAM\n"
+ "Z+C-Up:Format Z+C-Down:Erase Z+L:Corrupt\n");
+}
+
+enum program_state_type state_wait_action(joypad_buttons_t btn) {
+ // FIXME: properly handle non pressed button
+ if (btn.z) {
+ // risky actions can only be done with Z held
+ if (btn.a) {
+ return STATE_FSCK_FIX;
+ } else if (btn.b) {
+ return STATE_DELETE_FILE;
+ } else if (btn.r) {
+ return STATE_COPY_FROM_RAM;
+ } else if (btn.l) {
+ return STATE_CORRUPT;
+ } else if (btn.c_up) {
+ return STATE_FORMAT;
+ } else if (btn.c_down) {
+ return STATE_FORMAT_ERASE;
+ }
+ } else {
+ // regular actions
+ if (btn.a) {
+ return STATE_FSCK;
+ } else if (btn.b) {
+ return STATE_CREATE_FILE;
+ } else if (btn.r) {
+ return STATE_COPY_TO_RAM;
+ } else if (btn.start) {
+ return STATE_PRINT_PAK_OVERVIEW;
+ }
+ }
+
+ return STATE_WAIT_ACTION;
+}
+
+enum program_state_type state_print_connect_message(joypad_port_t port)
+{
+ console_clear();
+ print_working_controller_message(port);
+ printf("Please connect joypad %d and insert a controller pak into it.\n", port + 1);
+ console_render();
+
+ return STATE_WAIT_CONNECT;
+}
+
+enum program_state_type state_wait_connect(joypad_port_t port)
+{
+ if (joypad_is_connected(port) && joypad_get_accessory_type(port) == JOYPAD_ACCESSORY_TYPE_CONTROLLER_PAK) {
+ return STATE_PRINT_PAK_OVERVIEW;
+ }
+
+ return STATE_WAIT_CONNECT;
+}
+
+enum program_state_type state_print_pak_overview(joypad_port_t port, const char* prefix)
+{
+ console_clear();
+ print_working_controller_message(port);
+
+ // Reading the serial can be done before mounting the fs
+ uint8_t serial[24];
+ memset(serial, 0, 24);
+ int err = cpakfs_get_serial(port, serial);
+ if (err < 0) {
+ printf("Unable to read serial (%d)\n", err);
+ printf("(%d) %s\n", errno, strerror(errno));
+ } else {
+ printf("Serial: %08"PRIx32"%08"PRIx32" %08"PRIx32"%08"PRIx32" %08"PRIx32"%08"PRIx32"\n",
+ *(const uint32_t*)(serial+0*4),
+ *(const uint32_t*)(serial+1*4),
+ *(const uint32_t*)(serial+2*4),
+ *(const uint32_t*)(serial+3*4),
+ *(const uint32_t*)(serial+4*4),
+ *(const uint32_t*)(serial+5*4));
+ }
+
+ err = cpakfs_mount(port, prefix);
+ if (err < 0) {
+ printf("Unable to mount cpak filesystem (%d)\n", err);
+ printf("(%d) %s\n", errno, strerror(errno));
+ print_actions();
+ console_render();
+ return STATE_WAIT_ACTION;
+ }
+
+ cpakfs_stats_t stats;
+ err = cpakfs_get_stats(port, &stats);
+ if (err < 0) {
+ printf("Unable to get cpak filesystem stats (%d)\n", err);
+ printf("(%d) %s\n", errno, strerror(errno));
+ } else {
+ printf("pages: %d / %d, notes: %d / %d, banks: %d\n",
+ stats.pages.used,
+ stats.pages.total,
+ stats.notes.used,
+ stats.notes.total,
+ stats.num_banks);
+ }
+
+ int idx = 0;
+ dir_t dir;
+ if (dir_findfirst(prefix, &dir) == 0) do {
+ char path[255 + 8];
+ uint8_t buf[8];
+ char buf_str[20];
+ FILE* f;
+
+ // read first few bytes of file content and print some hex dump of it
+ sprintf(path, "%s%s", prefix, dir.d_name);
+ memset(buf, 0, sizeof(buf));
+ f = fopen(path, "rb");
+ // a filename returned by directory iteration must be reachable by fopen
+ assert(f != NULL);
+
+ int nread = fread(buf, 1, sizeof(buf), f);
+ char* dst = buf_str;
+ for (int i = 0; i < nread; i++) {
+ int nbytes = sprintf(dst, "%02x", buf[i]);
+ if (nbytes <= 0) {
+ break;
+ }
+ dst += nbytes;
+ }
+ if (nread >= 8) {
+ sprintf(dst, "...");
+ }
+
+ fclose(f);
+
+ // should fit on a single line
+ printf("%02d - %-28s - %5"PRIi64" - %-19s\n", idx, dir.d_name, dir.d_size, buf_str);
+
+ ++idx;
+ } while(dir_findnext(prefix, &dir) == 0);
+
+ for (; idx < 16+1; idx++) {
+ printf("\n");
+ }
+
+ cpakfs_unmount(port);
+
+ print_actions();
+ console_render();
+ return STATE_WAIT_ACTION;
+}
+
+void report_issue(void *ctx, cpakfs_issue_t issue, cpakfs_issue_level_t level, const char *fmt, ...) {
+ int* nerr = (int*)ctx;
+ const int limit = 10;
+
+ // Only print the first 10 issues
+ if (*nerr >= limit) {
+ return;
+ }
+
+ const char* level_str;
+ switch(level) {
+ case CPAKFS_LEVEL_INFO: level_str = "INFO"; break;
+ case CPAKFS_LEVEL_WARNING: level_str = "WARN"; break;
+ case CPAKFS_LEVEL_ERROR: level_str = "ERROR"; break;
+ default: level_str = "???";
+ }
+
+ printf("[%s] ", level_str);
+
+ va_list arg;
+ va_start(arg, fmt);
+ vprintf(fmt, arg);
+ va_end(arg);
+
+ printf("\n");
+
+ (*nerr)++;
+
+ if (*nerr >= limit) {
+ printf("...\n");
+ }
+}
+
+enum program_state_type state_fsck(joypad_port_t port, bool fix) {
+ console_clear();
+ print_working_controller_message(port);
+ printf("%s pak\n", (fix ? "Fixing" : "Validating"));
+ // give early feedback to user in case operation takes time
+ console_render();
+
+ int nerr = 0;
+ int err = cpakfs_fsck(port, fix, report_issue, &nerr);
+ if (err < 0) {
+ printf("Unable to %s file system (%d)\n", (fix ? "fix" : "validate"), err);
+ printf("(%d) %s\n", errno, strerror(errno));
+ } else {
+ printf("%s %d errors !\n", (fix ? "Fixed" : "Found"), err);
+ }
+
+ print_actions();
+ console_render();
+ return STATE_WAIT_ACTION;
+}
+
+enum program_state_type state_format(joypad_port_t port, bool erase) {
+ console_clear();
+ print_working_controller_message(port);
+ printf("Formatting pak%s\n", (erase ? " and purge all pages content" : ""));
+ // give early feedback to user because operation takes time
+ console_render();
+
+ int err = cpakfs_format(port, erase);
+ if (err < 0) {
+ printf("Unable to format pak(%d)\n", err);
+ printf("(%d) %s\n", errno, strerror(errno));
+ } else {
+ printf("Formatting done!\n");
+ }
+
+ print_actions();
+ console_render();
+ return STATE_WAIT_ACTION;
+}
+
+enum program_state_type state_corrupt(joypad_port_t port) {
+ console_clear();
+ print_working_controller_message(port);
+ printf("Corrupting\n");
+ console_render();
+
+ // Overwrite the first five pages of first bank to corrupt fs.
+ // This matches the behavior of old cpaktest example.
+ // But I'm not sure how useful that is...
+ int err = 0;
+ uint8_t buffer[256];
+ memset(buffer, 0xff, sizeof(buffer));
+
+ for (int i = 0; i < 5; i++) {
+ printf("Erasing page %d\n", i);
+ console_render();
+ err = cpak_write(port, 0, i * 256, buffer, 256);
+ if (err < 0) {
+ printf("Unable to write to pak(%d)\n", err);
+ printf("(%d) %s\n", errno, strerror(errno));
+ break;
+ }
+ }
+
+ if (err == 0) {
+ printf("Pak has been corrupted\n");
+ }
+
+ print_actions();
+ console_render();
+ return STATE_WAIT_ACTION;
+}
+
+enum program_state_type state_copy_to_ram(joypad_port_t port, uint8_t* buffer, size_t* size) {
+ console_clear();
+ print_working_controller_message(port);
+ printf("Copying to RAM\n");
+
+ // First we need to determine how many banks there is
+ // We use the bank probing method to avoid relying on fs-level
+ // statistics.
+ printf("Probing number of banks\n");
+ console_render();
+ int banks = cpak_probe_banks(port);
+ if (banks < 0) {
+ printf("Unable to probe number of banks(%d)\n", banks);
+ printf("(%d) %s\n", errno, strerror(errno));
+ print_actions();
+ console_render();
+ return STATE_WAIT_ACTION;
+ }
+ printf("Found %d banks\n", banks);
+ console_render();
+
+ // Copy loop done in chunks that don't cross the bank boundary.
+ // Assume that when cpak_read succeed the number of bytes read
+ // equals the number passed as argument (and not less).
+ // This is the case in the current cpak_read implementation but
+ // is not documented as such in the public API documentation.
+ size_t pak_size = banks * BANK_SIZE;
+ uint8_t bank = 0;
+ printf("Copying %u bytes\n", pak_size);
+ uint64_t t0 = get_ticks_ms();
+ while (bank < banks) {
+ int nbytes = cpak_read(port, bank, 0, buffer + bank * BANK_SIZE, BANK_SIZE);
+ if (nbytes < 0) {
+ printf("\n");
+ printf("Unable to read pak(%d)\n", nbytes);
+ printf("(%d) %s\n", errno, strerror(errno));
+ break;
+ }
+ assert(nbytes == BANK_SIZE);
+ bank++;
+
+ // Primitive way of showing progress
+ printf(".");
+ console_render();
+ }
+ uint64_t t1 = get_ticks_ms();
+ printf("\n");
+
+ *size = bank * BANK_SIZE;
+
+ int64_t duration = t1 - t0;
+ printf("%u bytes copied in %"PRIi64" ms: %.2fkb/s\n", bank * BANK_SIZE, duration, (1. * (bank * BANK_SIZE))/duration);
+
+ print_actions();
+ console_render();
+ return STATE_WAIT_ACTION;
+}
+
+enum program_state_type state_copy_from_ram(joypad_port_t port, const uint8_t* buffer, size_t size) {
+ console_clear();
+ print_working_controller_message(port);
+ printf("Copying from RAM\n");
+
+ // Early back off if RAM buffer is empty
+ if (size == 0) {
+ printf("Nothing to copy from.\nPlease first copy pak to RAM using R button\n");
+ print_actions();
+ console_render();
+ return STATE_WAIT_ACTION;
+ }
+
+ // First we need to determine how many banks there is
+ // We use the bank probing method to avoid relying on fs-level
+ // statistics.
+ printf("Probing number of banks\n");
+ console_render();
+ int banks = cpak_probe_banks(port);
+ if (banks < 0) {
+ printf("Unable to probe number of banks(%d)\n", banks);
+ printf("(%d) %s\n", errno, strerror(errno));
+ print_actions();
+ console_render();
+ return STATE_WAIT_ACTION;
+ }
+ printf("Found %d banks\n", banks);
+ console_render();
+
+ size_t pak_size = banks * BANK_SIZE;
+
+ // Ensure that sizes matches
+ if (size != pak_size) {
+ printf("Pak size (%u) differs from size in RAM (%u)\n", pak_size, size);
+ print_actions();
+ console_render();
+ return STATE_WAIT_ACTION;
+ }
+
+ // Copy loop done in chunks that don't cross the bank boundary.
+ // Assume that when cpak_write succeed the number of bytes written
+ // equals the number passed as argument (and not less).
+ // This is the case in the current cpak_write implementation but
+ // is not documented as such in the public API documentation.
+ uint8_t bank = 0;
+ printf("Copying %u bytes\n", size);
+ uint64_t t0 = get_ticks_ms();
+ while (bank < banks) {
+ int nbytes = cpak_write(port, bank, 0, buffer + bank * BANK_SIZE, BANK_SIZE);
+ if (nbytes < 0) {
+ printf("\n");
+ printf("Unable to write pak(%d)\n", nbytes);
+ printf("(%d) %s\n", errno, strerror(errno));
+ break;
+ }
+ assert(nbytes == BANK_SIZE);
+ bank++;
+
+ // Primitive way of showing progress
+ printf(".");
+ console_render();
+ }
+ uint64_t t1 = get_ticks_ms();
+ printf("\n");
+
+ int64_t duration = t1 - t0;
+ printf("%u bytes copied in %"PRIi64" ms: %.2fkb/s\n", bank * BANK_SIZE, duration, (1. * (bank * BANK_SIZE))/duration);
+
+ print_actions();
+ console_render();
+ return STATE_WAIT_ACTION;
+}
+
+enum program_state_type state_create_file(joypad_port_t port, const char* prefix, const char* fname) {
+ console_clear();
+ print_working_controller_message(port);
+ printf("Creating file\n");
+
+ bool ok = true;
+
+ int err = cpakfs_mount(port, prefix);
+ if (err < 0) {
+ ok = false;
+ printf("Unable to mount cpak filesystem (%d)\n", err);
+ printf("(%d) %s\n", errno, strerror(errno));
+ print_actions();
+ console_render();
+ return STATE_WAIT_ACTION;
+ }
+
+ char path[255 + 8];
+ sprintf(path, "%s%s", prefix, fname);
+
+ printf("Opening file %s\n", fname);
+ FILE *f = fopen(path, "wb");
+ if (f == NULL) {
+ ok = false;
+ printf("Unable to open file\n");
+ printf("(%d) %s\n", errno, strerror(errno));
+ } else {
+ const uint8_t content[] = {
+ 'c', 'p', 'a', 'k', '_', 'd', 'e', 'm', 'o',
+ };
+ size_t size = sizeof(content);
+
+ printf("Writing content\n");
+ int nwritten = fwrite(content, 1, size, f);
+ if (nwritten != size) {
+ ok = false;
+ printf("Unable to write requested amount of bytes: %u != %u\n", nwritten, size);
+ printf("(%d) %s\n", errno, strerror(errno));
+ }
+
+ err = fclose(f);
+ if (err < 0) {
+ ok = false;
+ printf("Unable to close file (%d)\n", err);
+ printf("(%d) %s\n", errno, strerror(errno));
+ }
+ }
+
+ err = cpakfs_unmount(port);
+ if (err < 0) {
+ ok = false;
+ printf("Unable to unmount filesystem (%d)\n", err);
+ printf("(%d) %s\n", errno, strerror(errno));
+ }
+
+ if (ok) {
+ printf("File %s created\n", fname);
+ }
+
+ print_actions();
+ console_render();
+ return STATE_WAIT_ACTION;
+}
+
+
+enum program_state_type state_delete_file(joypad_port_t port, const char* prefix, const char* fname) {
+ console_clear();
+ print_working_controller_message(port);
+ printf("Deleting file\n");
+
+ bool ok = true;
+
+ int err = cpakfs_mount(port, prefix);
+ if (err < 0) {
+ ok = false;
+ printf("Unable to mount cpak filesystem (%d)\n", err);
+ printf("(%d) %s\n", errno, strerror(errno));
+ print_actions();
+ console_render();
+ return STATE_WAIT_ACTION;
+ }
+
+ char path[255 + 8];
+ sprintf(path, "%s%s", prefix, fname);
+
+ err = remove(path);
+ if (err != 0) {
+ ok = false;
+ printf("Unable to remove file (%d)\n", err);
+ printf("(%d) %s\n", errno, strerror(errno));
+ }
+
+ err = cpakfs_unmount(port);
+ if (err < 0) {
+ ok = false;
+ printf("Unable to unmount filesystem (%d)\n", err);
+ printf("(%d) %s\n", errno, strerror(errno));
+ }
+
+ if (ok) {
+ printf("File %s deleted\n", fname);
+ }
+
+ print_actions();
+ console_render();
+ return STATE_WAIT_ACTION;
+}
+
+
+int main(void)
+{
+ // Only handle controller pak from player 1
+ const joypad_port_t port = JOYPAD_PORT_1;
+
+ // Recommended prefix is 'cpakN:/' with N being the player number (1..4)
+ char prefix[8];
+ sprintf(prefix, "cpak%d:/", port + 1);
+
+ // Full name must respect the GAME.PU-filename.ext format.
+ // To reduce name collision with other games,
+ // you should use some unique game and publisher code, be creative !
+ // Especially we do not recommend to use the same gamecode
+ // as the ROM header because most homebrews would then use
+ // "NED\x00" gamecode and collision would be inevitable.
+ const char* fname = "DRAG.ON-cpakdemo.1";
+
+ console_init();
+ joypad_init();
+
+ console_set_render_mode(RENDER_MANUAL);
+ console_clear();
+
+ size_t pak_content_size = 0;
+ enum program_state_type state = STATE_PRINT_CONNECT_MSG;
+ while (1)
+ {
+ joypad_poll();
+ joypad_buttons_t btn = joypad_get_buttons(port);
+
+ // Handle joypad / accessory disconnection
+ if (state > STATE_WAIT_CONNECT) {
+ if (!joypad_is_connected(port) || joypad_get_accessory_type(port) != JOYPAD_ACCESSORY_TYPE_CONTROLLER_PAK) {
+ // may fail if cpakfs wasn't mounted yet
+ cpakfs_unmount(port);
+ state = STATE_PRINT_CONNECT_MSG;
+ }
+ }
+
+ switch(state) {
+ case STATE_PRINT_CONNECT_MSG:
+ state = state_print_connect_message(port);
+ break;
+
+ case STATE_WAIT_CONNECT:
+ state = state_wait_connect(port);
+ break;
+
+ case STATE_PRINT_PAK_OVERVIEW:
+ state = state_print_pak_overview(port, prefix);
+ break;
+
+ case STATE_WAIT_ACTION:
+ state = state_wait_action(btn);
+ break;
+
+ case STATE_FSCK:
+ case STATE_FSCK_FIX:
+ bool fix = (state == STATE_FSCK_FIX);
+ state = state_fsck(port, fix);
+ break;
+
+ case STATE_FORMAT:
+ case STATE_FORMAT_ERASE:
+ bool erase = (state == STATE_FORMAT_ERASE);
+ state = state_format(port, erase);
+ break;
+
+ case STATE_CORRUPT:
+ state = state_corrupt(port);
+ break;
+
+ case STATE_COPY_TO_RAM:
+ state = state_copy_to_ram(port, pak_content, &pak_content_size);
+ break;
+
+ case STATE_COPY_FROM_RAM:
+ state = state_copy_from_ram(port, pak_content, pak_content_size);
+ break;
+
+ case STATE_CREATE_FILE:
+ state = state_create_file(port, prefix, fname);
+ break;
+
+ case STATE_DELETE_FILE:
+ state = state_delete_file(port, prefix, fname);
+ break;
+
+ default:
+ }
+ }
+}
diff --git a/examples/cpaktest/Makefile b/examples/cpaktest/Makefile
deleted file mode 100644
index 295145ba1..000000000
--- a/examples/cpaktest/Makefile
+++ /dev/null
@@ -1,17 +0,0 @@
-all: cpaktest.z64
-.PHONY: all
-
-BUILD_DIR = build
-include $(N64_INST)/include/n64.mk
-
-OBJS = $(BUILD_DIR)/cpaktest.o
-
-cpaktest.z64: N64_ROM_TITLE = "ControllerPak Test"
-
-$(BUILD_DIR)/cpaktest.elf: $(OBJS)
-
-clean:
- rm -rf $(BUILD_DIR) *.z64
-.PHONY: clean
-
--include $(wildcard $(BUILD_DIR)/*.d)
diff --git a/examples/cpaktest/cpaktest.c b/examples/cpaktest/cpaktest.c
deleted file mode 100644
index 9b334d52b..000000000
--- a/examples/cpaktest/cpaktest.c
+++ /dev/null
@@ -1,241 +0,0 @@
-#include
-#include
-#include
-#include
-#include
-
-static uint8_t mempak_data[128 * MEMPAK_BLOCK_SIZE];
-
-int main(void)
-{
- /* Initialize peripherals */
- console_init();
- joypad_init();
-
- console_set_render_mode(RENDER_MANUAL);
-
- console_clear();
-
- printf(
- "To test an inserted\n"
- "ControllerPak (mempak):\n\n"
- "Press A to read Pak.\n\n"
- "Press B to format Pak.\n\n"
- "Press Z to corrupt Pak.\n\n"
- "Press L to copy Pak.\n\n" /* CPak to RAM */
- "Press R to paste Pak." /* RAM to CPak */
- );
-
- console_render();
-
- /* Main loop test */
- while (1)
- {
- /* To do initialize routines */
- joypad_poll();
-
-
- JOYPAD_PORT_FOREACH(port)
- {
- joypad_buttons_t keys = joypad_get_buttons_pressed(port);
-
- if (keys.a)
- {
- console_clear();
-
- /* Make sure they don't have a Rumble Pak inserted instead */
- switch (joypad_get_accessory_type(port))
- {
- case JOYPAD_ACCESSORY_TYPE_NONE:
- printf("No accessory inserted!");
- break;
- case JOYPAD_ACCESSORY_TYPE_CONTROLLER_PAK:
- {
- int err;
- if ( (err = validate_mempak(port)) )
- {
- if (err == -3)
- {
- printf("Pak is not formatted!");
- }
- else
- {
- printf("Pak bad or removed during read!");
- }
- }
- else
- {
- for (int j = 0; j < 16; j++)
- {
- entry_structure_t entry;
-
- get_mempak_entry(port, j, &entry);
-
- if (entry.valid)
- {
- printf("%s - %d blocks\n", entry.name, entry.blocks);
- }
- else
- {
- printf("(EMPTY)\n");
- }
- }
-
- printf("\nFree space: %d blocks", get_mempak_free_space(port));
- }
-
- break;
- }
- default:
- printf("Cannot read data from this accessory!");
- break;
- }
-
- console_render();
- }
- else if (keys.b)
- {
- console_clear();
-
- /* Make sure they don't have a Rumble Pak inserted instead */
- switch (joypad_get_accessory_type(port))
- {
- case JOYPAD_ACCESSORY_TYPE_NONE:
- printf("No accessory inserted!");
- break;
- case JOYPAD_ACCESSORY_TYPE_CONTROLLER_PAK:
- if (format_mempak(port))
- {
- printf( "Error formatting Pak!" );
- }
- else
- {
- printf("Pak formatted!");
- }
-
- break;
- default:
- printf("Cannot format this accessory!");
- break;
- }
-
- console_render();
- }
- else if (keys.z)
- {
- console_clear();
-
- /* Make sure they don't have a rumble pak inserted instead */
- switch (joypad_get_accessory_type(port))
- {
- case JOYPAD_ACCESSORY_TYPE_NONE:
- printf("No accessory inserted!");
- break;
- case JOYPAD_ACCESSORY_TYPE_CONTROLLER_PAK:
- {
- uint8_t sector[256];
- int err = 0;
-
- memset(sector, 0xFF, 256);
-
- err |= write_mempak_sector(port, 0, sector);
- err |= write_mempak_sector(port, 1, sector);
- err |= write_mempak_sector(port, 2, sector);
- err |= write_mempak_sector(port, 3, sector);
- err |= write_mempak_sector(port, 4, sector);
-
- if (err)
- {
- printf( "Error corrupting data!" );
- }
- else
- {
- printf( "Data corrupted on Pak!" );
- }
-
- break;
- }
- default:
- printf("Cannot erase data from this accessory!");
- break;
- }
-
- console_render();
- }
- else if (keys.l)
- {
- console_clear();
-
- /* Make sure they don't have a rumble pak inserted instead */
- switch (joypad_get_accessory_type(port))
- {
- case JOYPAD_ACCESSORY_TYPE_NONE:
- printf("No accessory inserted!");
- break;
- case JOYPAD_ACCESSORY_TYPE_CONTROLLER_PAK:
- {
- int err = 0;
-
- for (int j = 0; j < 128; j++)
- {
- err |= read_mempak_sector(port, j, &mempak_data[j * MEMPAK_BLOCK_SIZE]);
- }
-
- if (err)
- {
- printf("Error loading data!");
- }
- else
- {
- printf("Data loaded into RAM!");
- }
-
- break;
- }
- default:
- printf("Cannot copy data from this accessory!");
- break;
- }
-
- console_render();
- }
- else if (keys.r)
- {
- console_clear();
-
- /* Make sure they don't have a rumble pak inserted instead */
- switch (joypad_get_accessory_type(port))
- {
- case JOYPAD_ACCESSORY_TYPE_NONE:
- printf("No accessory inserted!");
- break;
- case JOYPAD_ACCESSORY_TYPE_CONTROLLER_PAK:
- {
- int err = 0;
-
- for (int j = 0; j < 128; j++)
- {
- err |= write_mempak_sector(port, j, &mempak_data[j * MEMPAK_BLOCK_SIZE]);
- }
-
- if (err)
- {
- printf("Error saving data!");
- }
- else
- {
- printf("Data saved onto Pak!");
- }
-
- break;
- }
- default:
- printf("Cannot paste data to this accessory!");
- break;
- }
-
- console_render();
- }
- }
- }
-}
diff --git a/examples/ctest/ctest.c b/examples/ctest/ctest.c
index ac1552a57..b95756b40 100644
--- a/examples/ctest/ctest.c
+++ b/examples/ctest/ctest.c
@@ -67,7 +67,7 @@ int main(void)
joypad_set_rumble_active(port, true);
}
- if (pressed.a)
+ if (pressed.b)
{
joypad_set_rumble_active(port, false);
}
diff --git a/examples/customfont/.gitignore b/examples/customfont/.gitignore
new file mode 100644
index 000000000..87ef66815
--- /dev/null
+++ b/examples/customfont/.gitignore
@@ -0,0 +1 @@
+filesystem/
\ No newline at end of file
diff --git a/examples/customfont/Makefile b/examples/customfont/Makefile
index 3c6542f56..765e033b3 100644
--- a/examples/customfont/Makefile
+++ b/examples/customfont/Makefile
@@ -20,7 +20,7 @@ $(BUILD_DIR)/customfont.dfs: filesystem/libdragon-font.sprite
$(BUILD_DIR)/customfont.elf: $(OBJS)
clean:
- rm -rf $(BUILD_DIR) *.z64
+ rm -rf $(BUILD_DIR) *.z64 filesystem
.PHONY: clean
-include $(DEPS)
diff --git a/examples/customfont/filesystem/libdragon-font.png b/examples/customfont/assets/libdragon-font.png
similarity index 100%
rename from examples/customfont/filesystem/libdragon-font.png
rename to examples/customfont/assets/libdragon-font.png
diff --git a/examples/customfont/filesystem/libdragon-font.sprite b/examples/customfont/filesystem/libdragon-font.sprite
deleted file mode 100644
index e75fd576f..000000000
Binary files a/examples/customfont/filesystem/libdragon-font.sprite and /dev/null differ
diff --git a/examples/dfsdemo/dfsdemo.c b/examples/dfsdemo/dfsdemo.c
index 3d8222aaa..e4cb793a0 100644
--- a/examples/dfsdemo/dfsdemo.c
+++ b/examples/dfsdemo/dfsdemo.c
@@ -5,6 +5,7 @@
#include
#include
#include
+#include
#define MAX_LIST 20
@@ -12,6 +13,7 @@ typedef struct
{
uint32_t type;
char filename[MAX_FILENAME_LEN+1];
+ char fullpath[MAX_FILENAME_LEN+1];
} direntry_t;
char dir[512] = "rom://";
@@ -95,6 +97,8 @@ direntry_t *populate_dir(int *count)
{
list[(*count)-1].type = buf.d_type;
strcpy(list[(*count)-1].filename, buf.d_name);
+ strcpy(list[(*count)-1].fullpath, dir);
+ strcat(list[(*count)-1].fullpath, buf.d_name);
/* Grab next */
ret = dir_findnext(dir,&buf);
@@ -209,6 +213,30 @@ void display_dir(direntry_t *list, int cursor, int page, int max, int count)
}
}
+void display_file_info(direntry_t *list, int cursor)
+{
+ if (list == NULL) return;
+
+ /* File info display */
+ printf("\n");
+ printf("Info for '%s':\n", list[cursor].fullpath);
+
+ struct stat s;
+ if (stat(list[cursor].fullpath, &s) != 0)
+ {
+ printf("stat failed...\n");
+ return;
+ };
+
+ printf(" Type: ");
+ switch (s.st_mode & S_IFMT) {
+ case S_IFDIR: printf("Directory\n"); break;
+ case S_IFREG: printf("Regular file\n"); break;
+ default: printf("Unknown\n"); break;
+ }
+ printf(" Size: 0x%X\n",(int)(s.st_size));
+}
+
int main(void)
{
/* Initialize video */
@@ -237,6 +265,7 @@ int main(void)
{
console_clear();
display_dir(list, cursor, page, MAX_LIST, count);
+ display_file_info(list, cursor);
console_render();
joypad_poll();
diff --git a/examples/eepromfstest/eepromfstest.c b/examples/eepromfstest/eepromfstest.c
index c6ecab601..1864e26fe 100644
--- a/examples/eepromfstest/eepromfstest.c
+++ b/examples/eepromfstest/eepromfstest.c
@@ -32,7 +32,7 @@ typedef struct
int16_t map_pos_y;
uint16_t max_hp;
uint16_t current_hp;
- uint8_t inventory[256];
+ uint8_t inventory[64];
} game_save_state_t;
static void press_a_to_continue(void)
@@ -133,7 +133,7 @@ static int validate_game_high_scores(char * path)
int result;
result = read_game_high_scores(path);
- if ( result != 0 ) return result;
+ if ( result != 0 ) printf("Invalid high scores!\n");
press_a_to_continue();
result = write_game_high_scores(path);
@@ -200,7 +200,7 @@ static int validate_game_settings(char * path)
int result;
result = read_game_settings(path);
- if ( result != 0 ) return result;
+ if ( result != 0 ) printf("Invalid settings! (error: %d)\n", result);
press_a_to_continue();
result = write_game_settings(path);
@@ -268,7 +268,7 @@ static int write_game_save_state(char * path)
-120,
40,
36,
- { 1, 22, [254] = 52 }
+ { 1, 22, [63] = 52 }
};
printf( "Writing '%s'\n", path );
@@ -291,7 +291,7 @@ static int validate_game_save_state(char * path)
int result;
result = read_game_save_state(path);
- if ( result != 0 ) return result;
+ if ( result != 0 ) printf("Invalid save state! (error: %d)\n", result);
press_a_to_continue();
result = write_game_save_state(path);
@@ -367,12 +367,12 @@ static int validate_eeprom_16k(void)
{
int result;
const eepfs_entry_t eeprom_16k_files[] = {
- { "high_scores.dat", sizeof(game_high_scores) },
- { "settings.dat", sizeof(game_settings_t) },
- { "saves/slot1.sav", sizeof(game_save_state_t) },
- { "saves/slot2.sav", sizeof(game_save_state_t) },
- { "saves/slot3.sav", sizeof(game_save_state_t) },
- { "saves/slot4.sav", sizeof(game_save_state_t) }
+ { .path ="high_scores.dat", .size = sizeof(game_high_scores), .checksum = true, .backup = true },
+ { .path ="settings.dat", .size = sizeof(game_settings_t), .checksum = true, .backup = true },
+ { .path ="saves/slot1.sav", .size = sizeof(game_save_state_t), .checksum = true, .backup = true },
+ { .path ="saves/slot2.sav", .size = sizeof(game_save_state_t), .checksum = true, .backup = true },
+ { .path ="saves/slot3.sav", .size = sizeof(game_save_state_t), .checksum = true, .backup = true },
+ { .path ="saves/slot4.sav", .size = sizeof(game_save_state_t), .checksum = true, .backup = true }
};
printf( "EEPROM Detected: 16 Kibit (256 blocks)\n" );
@@ -452,11 +452,15 @@ static int validate_eeprom(eeprom_type_t eeprom_type)
int main(void)
{
+ debug_init_emulog();
+ debug_init_usblog();
+
/* Initialize peripherals */
console_init();
joypad_init();
console_set_render_mode(RENDER_AUTOMATIC);
+ console_set_debug(true);
console_clear();
const eeprom_type_t eeprom_type = eeprom_present();
diff --git a/examples/fontgallery/fontgallery.c b/examples/fontgallery/fontgallery.c
index 2f345f0df..cfb4b6e68 100644
--- a/examples/fontgallery/fontgallery.c
+++ b/examples/fontgallery/fontgallery.c
@@ -599,7 +599,7 @@ int main()
joypad_init();
dfs_init(DFS_DEFAULT_LOCATION);
- display_init(RESOLUTION_320x240, DEPTH_16_BPP, 3, GAMMA_NONE, FILTERS_RESAMPLE);
+ display_init(RESOLUTION_320x240, DEPTH_16_BPP, 3, GAMMA_NONE, FILTERS_DISABLED);
rdpq_init();
// rdpq_debug_start();
diff --git a/examples/gldemo/.gitignore b/examples/gldemo/.gitignore
new file mode 100644
index 000000000..87ef66815
--- /dev/null
+++ b/examples/gldemo/.gitignore
@@ -0,0 +1 @@
+filesystem/
\ No newline at end of file
diff --git a/examples/gldemo/Makefile b/examples/gldemo/Makefile
new file mode 100644
index 000000000..21ccb5828
--- /dev/null
+++ b/examples/gldemo/Makefile
@@ -0,0 +1,31 @@
+BUILD_DIR=build
+# This example uses OpenGL, which is a preview API
+LIBDRAGON_PREVIEW = 2
+include $(N64_INST)/include/n64.mk
+
+src = gldemo.c
+assets_png = $(wildcard assets/*.png)
+
+assets_conv = $(addprefix filesystem/,$(notdir $(assets_png:%.png=%.sprite)))
+
+MKSPRITE_FLAGS ?=
+
+all: gldemo.z64
+
+filesystem/%.sprite: assets/%.png
+ @mkdir -p $(dir $@)
+ @echo " [SPRITE] $@"
+ @$(N64_MKSPRITE) -f RGBA16 --compress -o "$(dir $@)" "$<"
+
+$(BUILD_DIR)/gldemo.dfs: $(assets_conv)
+$(BUILD_DIR)/gldemo.elf: $(src:%.c=$(BUILD_DIR)/%.o)
+
+gldemo.z64: N64_ROM_TITLE="GL Demo"
+gldemo.z64: $(BUILD_DIR)/gldemo.dfs
+
+clean:
+ rm -rf $(BUILD_DIR) filesystem/ gldemo.z64
+
+-include $(wildcard $(BUILD_DIR)/*.d)
+
+.PHONY: all clean
diff --git a/examples/gldemo/assets/circle0.png b/examples/gldemo/assets/circle0.png
new file mode 100644
index 000000000..8a4adb6cc
Binary files /dev/null and b/examples/gldemo/assets/circle0.png differ
diff --git a/examples/gldemo/assets/diamond0.png b/examples/gldemo/assets/diamond0.png
new file mode 100644
index 000000000..3d1d4363b
Binary files /dev/null and b/examples/gldemo/assets/diamond0.png differ
diff --git a/examples/gldemo/assets/pentagon0.png b/examples/gldemo/assets/pentagon0.png
new file mode 100644
index 000000000..e02eca490
Binary files /dev/null and b/examples/gldemo/assets/pentagon0.png differ
diff --git a/examples/gldemo/assets/triangle0.png b/examples/gldemo/assets/triangle0.png
new file mode 100644
index 000000000..2f2402af3
Binary files /dev/null and b/examples/gldemo/assets/triangle0.png differ
diff --git a/examples/gldemo/camera.h b/examples/gldemo/camera.h
new file mode 100644
index 000000000..41dc2e450
--- /dev/null
+++ b/examples/gldemo/camera.h
@@ -0,0 +1,20 @@
+#ifndef CAMERA_H
+#define CAMERA_H
+
+typedef struct {
+ float distance;
+ float rotation;
+} camera_t;
+
+void camera_transform(const camera_t *camera)
+{
+ // Set the camera transform
+ glLoadIdentity();
+ gluLookAt(
+ 0, -camera->distance, -camera->distance,
+ 0, 0, 0,
+ 0, 1, 0);
+ glRotatef(camera->rotation, 0, 1, 0);
+}
+
+#endif
diff --git a/examples/gldemo/cube.h b/examples/gldemo/cube.h
new file mode 100644
index 000000000..50e05c5ab
--- /dev/null
+++ b/examples/gldemo/cube.h
@@ -0,0 +1,101 @@
+#ifndef CUBE_H
+#define CUBE_H
+
+#include
+#include
+#include "vertex.h"
+
+static const float cube_size = 3.0f;
+
+static const vertex_t cube_vertices[] = {
+ // +X
+ { .position = { cube_size, -cube_size, -cube_size}, .texcoord = {0.f, 0.f}, .normal = { 1.f, 0.f, 0.f}, .color = 0xFF0000FF },
+ { .position = { cube_size, cube_size, -cube_size}, .texcoord = {1.f, 0.f}, .normal = { 1.f, 0.f, 0.f}, .color = 0xFF0000FF },
+ { .position = { cube_size, cube_size, cube_size}, .texcoord = {1.f, 1.f}, .normal = { 1.f, 0.f, 0.f}, .color = 0xFF0000FF },
+ { .position = { cube_size, -cube_size, cube_size}, .texcoord = {0.f, 1.f}, .normal = { 1.f, 0.f, 0.f}, .color = 0xFF0000FF },
+
+ // -X
+ { .position = {-cube_size, -cube_size, -cube_size}, .texcoord = {0.f, 0.f}, .normal = {-1.f, 0.f, 0.f}, .color = 0x00FFFFFF },
+ { .position = {-cube_size, -cube_size, cube_size}, .texcoord = {0.f, 1.f}, .normal = {-1.f, 0.f, 0.f}, .color = 0x00FFFFFF },
+ { .position = {-cube_size, cube_size, cube_size}, .texcoord = {1.f, 1.f}, .normal = {-1.f, 0.f, 0.f}, .color = 0x00FFFFFF },
+ { .position = {-cube_size, cube_size, -cube_size}, .texcoord = {1.f, 0.f}, .normal = {-1.f, 0.f, 0.f}, .color = 0x00FFFFFF },
+
+ // +Y
+ { .position = {-cube_size, cube_size, -cube_size}, .texcoord = {0.f, 0.f}, .normal = { 0.f, 1.f, 0.f}, .color = 0x00FF00FF },
+ { .position = {-cube_size, cube_size, cube_size}, .texcoord = {0.f, 1.f}, .normal = { 0.f, 1.f, 0.f}, .color = 0x00FF00FF },
+ { .position = { cube_size, cube_size, cube_size}, .texcoord = {1.f, 1.f}, .normal = { 0.f, 1.f, 0.f}, .color = 0x00FF00FF },
+ { .position = { cube_size, cube_size, -cube_size}, .texcoord = {1.f, 0.f}, .normal = { 0.f, 1.f, 0.f}, .color = 0x00FF00FF },
+
+ // -Y
+ { .position = {-cube_size, -cube_size, -cube_size}, .texcoord = {0.f, 0.f}, .normal = { 0.f, -1.f, 0.f}, .color = 0xFF00FFFF },
+ { .position = { cube_size, -cube_size, -cube_size}, .texcoord = {1.f, 0.f}, .normal = { 0.f, -1.f, 0.f}, .color = 0xFF00FFFF },
+ { .position = { cube_size, -cube_size, cube_size}, .texcoord = {1.f, 1.f}, .normal = { 0.f, -1.f, 0.f}, .color = 0xFF00FFFF },
+ { .position = {-cube_size, -cube_size, cube_size}, .texcoord = {0.f, 1.f}, .normal = { 0.f, -1.f, 0.f}, .color = 0xFF00FFFF },
+
+ // +Z
+ { .position = {-cube_size, -cube_size, cube_size}, .texcoord = {0.f, 0.f}, .normal = { 0.f, 0.f, 1.f}, .color = 0x0000FFFF },
+ { .position = { cube_size, -cube_size, cube_size}, .texcoord = {1.f, 0.f}, .normal = { 0.f, 0.f, 1.f}, .color = 0x0000FFFF },
+ { .position = { cube_size, cube_size, cube_size}, .texcoord = {1.f, 1.f}, .normal = { 0.f, 0.f, 1.f}, .color = 0x0000FFFF },
+ { .position = {-cube_size, cube_size, cube_size}, .texcoord = {0.f, 1.f}, .normal = { 0.f, 0.f, 1.f}, .color = 0x0000FFFF },
+
+ // -Z
+ { .position = {-cube_size, -cube_size, -cube_size}, .texcoord = {0.f, 0.f}, .normal = { 0.f, 0.f, -1.f}, .color = 0xFFFF00FF },
+ { .position = {-cube_size, cube_size, -cube_size}, .texcoord = {0.f, 1.f}, .normal = { 0.f, 0.f, -1.f}, .color = 0xFFFF00FF },
+ { .position = { cube_size, cube_size, -cube_size}, .texcoord = {1.f, 1.f}, .normal = { 0.f, 0.f, -1.f}, .color = 0xFFFF00FF },
+ { .position = { cube_size, -cube_size, -cube_size}, .texcoord = {1.f, 0.f}, .normal = { 0.f, 0.f, -1.f}, .color = 0xFFFF00FF },
+};
+
+static const uint16_t cube_indices[] = {
+ 0, 1, 2, 0, 2, 3,
+ 4, 5, 6, 4, 6, 7,
+ 8, 9, 10, 8, 10, 11,
+ 12, 13, 14, 12, 14, 15,
+ 16, 17, 18, 16, 18, 19,
+ 20, 21, 22, 20, 22, 23,
+};
+
+void setup_cube()
+{
+}
+
+void draw_cube()
+{
+ glEnableClientState(GL_VERTEX_ARRAY);
+ glEnableClientState(GL_TEXTURE_COORD_ARRAY);
+ glEnableClientState(GL_NORMAL_ARRAY);
+ glEnableClientState(GL_COLOR_ARRAY);
+
+ glVertexPointer(3, GL_FLOAT, sizeof(vertex_t), (void*)(0*sizeof(float) + (void*)cube_vertices));
+ glTexCoordPointer(2, GL_FLOAT, sizeof(vertex_t), (void*)(3*sizeof(float) + (void*)cube_vertices));
+ glNormalPointer(GL_FLOAT, sizeof(vertex_t), (void*)(5*sizeof(float) + (void*)cube_vertices));
+ glColorPointer(4, GL_UNSIGNED_BYTE, sizeof(vertex_t), (void*)(8*sizeof(float) + (void*)cube_vertices));
+
+ glDrawElements(GL_TRIANGLES, sizeof(cube_indices) / sizeof(uint16_t), GL_UNSIGNED_SHORT, cube_indices);
+
+ glDisableClientState(GL_VERTEX_ARRAY);
+ glDisableClientState(GL_TEXTURE_COORD_ARRAY);
+ glDisableClientState(GL_NORMAL_ARRAY);
+ glDisableClientState(GL_COLOR_ARRAY);
+}
+
+void render_cube()
+{
+ rdpq_debug_log_msg("Cube");
+ glPushMatrix();
+ glTranslatef(0,-1.f,0);
+
+ // Apply vertex color as material color.
+ // Because the cube has colors set per vertex, we can color each face seperately
+ glEnable(GL_COLOR_MATERIAL);
+
+ // Apply to ambient and diffuse material properties
+ glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE);
+
+ draw_cube();
+
+ glDisable(GL_COLOR_MATERIAL);
+
+ glPopMatrix();
+}
+
+#endif
diff --git a/examples/gldemo/decal.h b/examples/gldemo/decal.h
new file mode 100644
index 000000000..e65ef43aa
--- /dev/null
+++ b/examples/gldemo/decal.h
@@ -0,0 +1,50 @@
+#ifndef DECAL_H
+#define DECAL_H
+
+#include
+#include
+
+void draw_quad()
+{
+ glBegin(GL_TRIANGLE_STRIP);
+ glNormal3f(0, 1, 0);
+ glTexCoord2f(0, 0);
+ glVertex3f(-0.5f, 0, -0.5f);
+ glTexCoord2f(0, 1);
+ glVertex3f(-0.5f, 0, 0.5f);
+ glTexCoord2f(1, 0);
+ glVertex3f(0.5f, 0, -0.5f);
+ glTexCoord2f(1, 1);
+ glVertex3f(0.5f, 0, 0.5f);
+ glEnd();
+}
+
+void render_decal()
+{
+ rdpq_debug_log_msg("Decal");
+ glPushMatrix();
+ glTranslatef(0, 0, 6);
+ glRotatef(35, 0, 1, 0);
+ glScalef(3, 3, 3);
+
+ // Decals are drawn with the depth func set to GL_EQUAL. Note that glPolygonOffset is not supported on N64.
+ glDepthFunc(GL_EQUAL);
+
+ // Disable writing to depth buffer, because the depth value will be the same anyway
+ glDepthMask(GL_FALSE);
+
+ // Apply vertex color as material color.
+ // This time, we set one vertex color for the entire model.
+ glEnable(GL_COLOR_MATERIAL);
+ glColor4f(1.0f, 0.4f, 0.2f, 0.5f);
+
+ draw_quad();
+
+ glDisable(GL_COLOR_MATERIAL);
+ glDepthMask(GL_TRUE);
+ glDepthFunc(GL_LESS);
+
+ glPopMatrix();
+}
+
+#endif
diff --git a/examples/gldemo/gldemo.c b/examples/gldemo/gldemo.c
new file mode 100644
index 000000000..86300985b
--- /dev/null
+++ b/examples/gldemo/gldemo.c
@@ -0,0 +1,331 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "camera.h"
+#include "cube.h"
+#include "decal.h"
+#include "sphere.h"
+#include "plane.h"
+#include "prim_test.h"
+#include "skinned.h"
+
+// Set this to 1 to enable rdpq debug output.
+// The demo will only run for a single frame and stop.
+#define DEBUG_RDP 0
+
+static uint32_t animation = 3283;
+static uint32_t texture_index = 0;
+static camera_t camera;
+
+static uint64_t frames = 0;
+
+static GLuint textures[4];
+
+static GLenum shade_model = GL_SMOOTH;
+static bool fog_enabled = false;
+
+static const GLfloat environment_color[] = { 0.1f, 0.03f, 0.2f, 1.f };
+
+static const GLfloat light_pos[8][4] = {
+ { 1, 0, 0, 0 },
+ { -1, 0, 0, 0 },
+ { 0, 0, 1, 0 },
+ { 0, 0, -1, 0 },
+ { 8, 3, 0, 1 },
+ { -8, 3, 0, 1 },
+ { 0, 3, 8, 1 },
+ { 0, 3, -8, 1 },
+};
+
+static const GLfloat light_diffuse[8][4] = {
+ { 1.0f, 0.0f, 0.0f, 1.0f },
+ { 0.0f, 1.0f, 0.0f, 1.0f },
+ { 0.0f, 0.0f, 1.0f, 1.0f },
+ { 1.0f, 1.0f, 0.0f, 1.0f },
+ { 1.0f, 0.0f, 1.0f, 1.0f },
+ { 0.0f, 1.0f, 1.0f, 1.0f },
+ { 1.0f, 1.0f, 1.0f, 1.0f },
+ { 1.0f, 1.0f, 1.0f, 1.0f },
+};
+
+static const char *texture_path[4] = {
+ "rom:/circle0.sprite",
+ "rom:/diamond0.sprite",
+ "rom:/pentagon0.sprite",
+ "rom:/triangle0.sprite",
+};
+
+static sprite_t *sprites[4];
+
+void setup()
+{
+ camera.distance = -10.0f;
+ camera.rotation = 0.0f;
+
+ for (uint32_t i = 0; i < 4; i++)
+ {
+ sprites[i] = sprite_load(texture_path[i]);
+ }
+
+ setup_sphere();
+ make_sphere_mesh();
+
+ setup_cube();
+
+ setup_plane();
+ make_plane_mesh();
+
+ float aspect_ratio = (float)display_get_width() / (float)display_get_height();
+ float near_plane = 1.0f;
+ float far_plane = 50.0f;
+
+ glMatrixMode(GL_PROJECTION);
+ glLoadIdentity();
+ glFrustum(-near_plane*aspect_ratio, near_plane*aspect_ratio, -near_plane, near_plane, near_plane, far_plane);
+
+ glMatrixMode(GL_MODELVIEW);
+ glLoadIdentity();
+
+ glLightModelfv(GL_LIGHT_MODEL_AMBIENT, environment_color);
+ glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER, GL_TRUE);
+
+ float light_radius = 10.0f;
+
+ for (uint32_t i = 0; i < 8; i++)
+ {
+ glEnable(GL_LIGHT0 + i);
+ glLightfv(GL_LIGHT0 + i, GL_DIFFUSE, light_diffuse[i]);
+ glLightf(GL_LIGHT0 + i, GL_LINEAR_ATTENUATION, 2.0f/light_radius);
+ glLightf(GL_LIGHT0 + i, GL_QUADRATIC_ATTENUATION, 1.0f/(light_radius*light_radius));
+ }
+
+ GLfloat mat_diffuse[] = { 1.0f, 1.0f, 1.0f, 1.0f };
+ glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, mat_diffuse);
+
+ glFogf(GL_FOG_START, 5);
+ glFogf(GL_FOG_END, 20);
+ glFogfv(GL_FOG_COLOR, environment_color);
+
+ glEnable(GL_MULTISAMPLE_ARB);
+
+ glGenTextures(4, textures);
+
+ #if 0
+ GLenum min_filter = GL_LINEAR_MIPMAP_LINEAR;
+ #else
+ GLenum min_filter = GL_LINEAR;
+ #endif
+
+
+ for (uint32_t i = 0; i < 4; i++)
+ {
+ glBindTexture(GL_TEXTURE_2D, textures[i]);
+
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter);
+
+ glSpriteTextureN64(GL_TEXTURE_2D, sprites[i], &(rdpq_texparms_t){.s.repeats = REPEAT_INFINITE, .t.repeats = REPEAT_INFINITE});
+ }
+}
+
+void set_light_positions(float rotation)
+{
+ glPushMatrix();
+ glRotatef(rotation*5.43f, 0, 1, 0);
+
+ for (uint32_t i = 0; i < 8; i++)
+ {
+ glLightfv(GL_LIGHT0 + i, GL_POSITION, light_pos[i]);
+ }
+ glPopMatrix();
+}
+
+void render()
+{
+ surface_t *disp = display_get();
+ surface_t *zbuf = display_get_zbuf();
+
+ rdpq_attach(disp, zbuf);
+
+ gl_context_begin();
+
+ glClearColor(environment_color[0], environment_color[1], environment_color[2], environment_color[3]);
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+ glMatrixMode(GL_MODELVIEW);
+ camera_transform(&camera);
+
+ float rotation = animation * 0.5f;
+
+ set_light_positions(rotation);
+
+ // Set some global render modes that we want to apply to all models
+ glEnable(GL_LIGHTING);
+ glEnable(GL_NORMALIZE);
+ glEnable(GL_DEPTH_TEST);
+ glEnable(GL_CULL_FACE);
+
+ glEnable(GL_TEXTURE_2D);
+ glBindTexture(GL_TEXTURE_2D, textures[texture_index]);
+
+ render_plane();
+ render_decal();
+ render_cube();
+ render_skinned(&camera, animation);
+
+ glBindTexture(GL_TEXTURE_2D, textures[(texture_index + 1)%4]);
+ render_sphere(rotation);
+
+ glDisable(GL_TEXTURE_2D);
+ glDisable(GL_LIGHTING);
+ render_primitives(rotation);
+
+ // Draw a primitive with GL_RDPQ_TEXTURING_N64
+ glEnable(GL_RDPQ_TEXTURING_N64);
+ glEnable(GL_RDPQ_MATERIAL_N64);
+
+ // When rendering with GL_RDPQ_TEXTURING_N64 we need to manualy specify the
+ // tile size and if a 0.5 offset should be used since the ucode itself cannot
+ // determine these. Here we set the tile size to be 32x32 and we apply an offset
+ // since we are using bilinear texture filtering
+ glTexSizeN64(32, 32);
+ rdpq_sprite_upload(TILE0, sprites[0], &(rdpq_texparms_t){.s.repeats = REPEAT_INFINITE, .t.repeats = REPEAT_INFINITE});
+ rdpq_set_mode_standard();
+ rdpq_mode_filter(FILTER_BILINEAR);
+
+ glBegin(GL_TRIANGLE_FAN);
+ glTexCoord2f(0.0f, 0.0f);
+ glVertex3f(-5.5f, 1.0f, -1.0f);
+
+ glTexCoord2f(0.0f, 1.0f);
+ glVertex3f(-5.5f, 1.0f, 1.0f);
+
+ glTexCoord2f(1.0f, 1.0f);
+ glVertex3f(-3.5f, 1.0f, 1.0f);
+
+ glTexCoord2f(1.0f, 0.0f);
+ glVertex3f(-3.5f, 1.0f, -1.0f);
+ glEnd();
+
+ glDisable(GL_RDPQ_TEXTURING_N64);
+ glDisable(GL_RDPQ_MATERIAL_N64);
+
+ gl_context_end();
+
+ rdpq_detach_show();
+
+ rspq_profile_next_frame();
+
+ if (((frames++) % 60) == 0) {
+ rspq_profile_dump();
+ rspq_profile_reset();
+ debugf("frame %lld\n", frames);
+ }
+}
+
+int main()
+{
+ debug_init_emulog();
+ debug_init_usblog();
+
+ dfs_init(DFS_DEFAULT_LOCATION);
+
+ display_init(RESOLUTION_320x240, DEPTH_16_BPP, 3, GAMMA_NONE, FILTERS_RESAMPLE_ANTIALIAS_DEDITHER);
+
+ rdpq_init();
+ gl_init();
+
+#if DEBUG_RDP
+ rdpq_debug_start();
+ rdpq_debug_log(true);
+#endif
+
+ setup();
+
+ joypad_init();
+
+ rspq_profile_start();
+
+#if !DEBUG_RDP
+ while (1)
+#endif
+ {
+ joypad_poll();
+ joypad_buttons_t pressed = joypad_get_buttons_pressed(JOYPAD_PORT_1);
+ joypad_buttons_t held = joypad_get_buttons_held(JOYPAD_PORT_1);
+ joypad_inputs_t inputs = joypad_get_inputs(JOYPAD_PORT_1);
+
+ if (held.a) {
+ animation++;
+ }
+
+ if (held.b) {
+ animation--;
+ }
+
+ if (pressed.start) {
+ debugf("%ld\n", animation);
+ }
+
+ if (pressed.r) {
+ shade_model = shade_model == GL_SMOOTH ? GL_FLAT : GL_SMOOTH;
+ glShadeModel(shade_model);
+ }
+
+ if (pressed.l) {
+ fog_enabled = !fog_enabled;
+ if (fog_enabled) {
+ glEnable(GL_FOG);
+ } else {
+ glDisable(GL_FOG);
+ }
+ }
+
+ if (pressed.c_up) {
+ if (sphere_rings < SPHERE_MAX_RINGS) {
+ sphere_rings++;
+ }
+
+ if (sphere_segments < SPHERE_MAX_SEGMENTS) {
+ sphere_segments++;
+ }
+
+ make_sphere_mesh();
+ }
+
+ if (pressed.c_down) {
+ if (sphere_rings > SPHERE_MIN_RINGS) {
+ sphere_rings--;
+ }
+
+ if (sphere_segments > SPHERE_MIN_SEGMENTS) {
+ sphere_segments--;
+ }
+
+ make_sphere_mesh();
+ }
+
+ if (pressed.c_right) {
+ texture_index = (texture_index + 1) % 4;
+ }
+
+ float y = inputs.stick_y / 128.f;
+ float x = inputs.stick_x / 128.f;
+ float mag = x*x + y*y;
+
+ if (fabsf(mag) > 0.01f) {
+ camera.distance += y * 0.2f;
+ camera.rotation = camera.rotation - x * 1.2f;
+ }
+
+ render();
+ if (DEBUG_RDP)
+ rspq_wait();
+ }
+
+}
diff --git a/examples/gldemo/plane.h b/examples/gldemo/plane.h
new file mode 100644
index 000000000..17898b53c
--- /dev/null
+++ b/examples/gldemo/plane.h
@@ -0,0 +1,122 @@
+#ifndef PLANE_H
+#define PLANE_H
+
+#include
+#include
+#include
+
+#include "vertex.h"
+
+#define PLANE_SIZE 20.0f
+#define PLANE_SEGMENTS 16
+
+static GLuint plane_buffers[2];
+static GLuint plane_array;
+static uint32_t plane_vertex_count;
+static uint32_t plane_index_count;
+
+void setup_plane()
+{
+ glGenBuffersARB(2, plane_buffers);
+
+ glGenVertexArrays(1, &plane_array);
+ glBindVertexArray(plane_array);
+
+ glEnableClientState(GL_VERTEX_ARRAY);
+ glEnableClientState(GL_TEXTURE_COORD_ARRAY);
+ glEnableClientState(GL_NORMAL_ARRAY);
+
+ glBindBufferARB(GL_ARRAY_BUFFER_ARB, plane_buffers[0]);
+
+ glVertexPointer(3, GL_FLOAT, sizeof(vertex_t), (void*)(0*sizeof(float)));
+ glTexCoordPointer(2, GL_FLOAT, sizeof(vertex_t), (void*)(3*sizeof(float)));
+ glNormalPointer(GL_FLOAT, sizeof(vertex_t), (void*)(5*sizeof(float)));
+
+ glBindBufferARB(GL_ARRAY_BUFFER_ARB, 0);
+
+ glBindVertexArray(0);
+}
+
+void make_plane_mesh()
+{
+ plane_vertex_count = (PLANE_SEGMENTS + 1) * (PLANE_SEGMENTS + 1);
+
+ glBindBufferARB(GL_ARRAY_BUFFER_ARB, plane_buffers[0]);
+ glBufferDataARB(GL_ARRAY_BUFFER_ARB, plane_vertex_count * sizeof(vertex_t), NULL, GL_STATIC_DRAW_ARB);
+
+ const float p0 = - (PLANE_SIZE / 2);
+ const float incr = PLANE_SIZE / PLANE_SEGMENTS;
+
+ vertex_t *vertices = glMapBufferARB(GL_ARRAY_BUFFER_ARB, GL_WRITE_ONLY_ARB);
+
+ for (uint32_t y = 0; y < PLANE_SEGMENTS + 1; y++)
+ {
+ for (uint32_t x = 0; x < PLANE_SEGMENTS + 1; x++)
+ {
+ uint32_t i = y * (PLANE_SEGMENTS + 1) + x;
+ vertex_t *v = &vertices[i];
+
+ v->position[0] = p0 + incr * x;
+ v->position[1] = 0;
+ v->position[2] = p0 + incr * y;
+
+ v->normal[0] = 0;
+ v->normal[1] = 1;
+ v->normal[2] = 0;
+
+ v->texcoord[0] = x;
+ v->texcoord[1] = y;
+ }
+ }
+
+ glUnmapBufferARB(GL_ARRAY_BUFFER_ARB);
+ glBindBufferARB(GL_ARRAY_BUFFER_ARB, 0);
+
+ plane_index_count = PLANE_SEGMENTS * PLANE_SEGMENTS * 6;
+
+ glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, plane_buffers[1]);
+ glBufferDataARB(GL_ELEMENT_ARRAY_BUFFER_ARB, plane_index_count * sizeof(uint16_t), NULL, GL_STATIC_DRAW_ARB);
+
+ uint16_t *indices = glMapBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, GL_WRITE_ONLY_ARB);
+
+ for (uint32_t y = 0; y < PLANE_SEGMENTS; y++)
+ {
+ for (uint32_t x = 0; x < PLANE_SEGMENTS; x++)
+ {
+ uint32_t i = (y * PLANE_SEGMENTS + x) * 6;
+
+ uint32_t row_start = y * (PLANE_SEGMENTS + 1);
+ uint32_t next_row_start = (y + 1) * (PLANE_SEGMENTS + 1);
+
+ indices[i + 0] = x + row_start;
+ indices[i + 1] = x + next_row_start;
+ indices[i + 2] = x + row_start + 1;
+ indices[i + 3] = x + next_row_start;
+ indices[i + 4] = x + next_row_start + 1;
+ indices[i + 5] = x + row_start + 1;
+ }
+ }
+
+ glUnmapBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB);
+ glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, 0);
+}
+
+void draw_plane()
+{
+ glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, plane_buffers[1]);
+ glBindVertexArray(plane_array);
+
+ glDrawElements(GL_TRIANGLES, plane_index_count, GL_UNSIGNED_SHORT, 0);
+
+ glBindVertexArray(0);
+ glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, 0);
+}
+
+void render_plane()
+{
+ rdpq_debug_log_msg("Plane");
+
+ draw_plane();
+}
+
+#endif
diff --git a/examples/gldemo/prim_test.h b/examples/gldemo/prim_test.h
new file mode 100644
index 000000000..9cc5c18e3
--- /dev/null
+++ b/examples/gldemo/prim_test.h
@@ -0,0 +1,208 @@
+#ifndef PRIM_TEST_H
+#define PRIM_TEST_H
+
+#include
+#include
+
+void points()
+{
+ glBegin(GL_POINTS);
+ glVertex2f(-1.f, -1.f);
+ glVertex2f(1.f, -1.f);
+ glVertex2f(1.f, 1.f);
+ glVertex2f(-1.f, 1.f);
+ glEnd();
+}
+
+void lines()
+{
+ glBegin(GL_LINES);
+ glVertex2f(-1.f, -1.f);
+ glVertex2f(1.f, -1.f);
+ glVertex2f(-1.f, 0.f);
+ glVertex2f(1.f, 0.f);
+ glVertex2f(-1.f, 1.f);
+ glVertex2f(1.f, 1.f);
+ glEnd();
+}
+
+void line_strip()
+{
+ glBegin(GL_LINE_STRIP);
+ glVertex2f(-1.f, -1.f);
+ glVertex2f(1.f, -1.f);
+ glVertex2f(1.f, 1.f);
+ glVertex2f(-1.f, 1.f);
+ glEnd();
+}
+
+void line_loop()
+{
+ glBegin(GL_LINE_LOOP);
+ glVertex2f(-1.f, -1.f);
+ glVertex2f(1.f, -1.f);
+ glVertex2f(1.f, 1.f);
+ glVertex2f(-1.f, 1.f);
+ glEnd();
+}
+
+void triangles()
+{
+ glBegin(GL_TRIANGLES);
+ glVertex2f(-1.f, -1.f);
+ glVertex2f(0.f, -1.f);
+ glVertex2f(-1.f, 0.f);
+
+ glVertex2f(1.f, 1.f);
+ glVertex2f(1.f, 0.f);
+ glVertex2f(0.f, 1.f);
+ glEnd();
+}
+
+void triangle_strip()
+{
+ glBegin(GL_TRIANGLE_STRIP);
+ glVertex2f(-1.f, -1.f);
+ glVertex2f(1.f, -1.f);
+ glVertex2f(-1.f, 1.f);
+ glVertex2f(1.f, 1.f);
+ glEnd();
+}
+
+void triangle_fan()
+{
+ glBegin(GL_TRIANGLE_FAN);
+ glVertex2f(0.f, 0.f);
+ glVertex2f(-1.f, 0.f);
+ glVertex2f(0.f, -1.f);
+ glVertex2f(1.f, 0.f);
+ glVertex2f(0.f, 1.f);
+ glVertex2f(-1.f, 0.f);
+ glEnd();
+}
+
+void quads()
+{
+ glBegin(GL_QUADS);
+ glVertex2f(-1.f, -1.f);
+ glVertex2f(0.f, -1.f);
+ glVertex2f(0.f, 0.f);
+ glVertex2f(-1.f, 0.f);
+
+ glVertex2f(1.f, 1.f);
+ glVertex2f(0.f, 1.f);
+ glVertex2f(0.f, 0.f);
+ glVertex2f(1.f, 0.f);
+ glEnd();
+}
+
+void quad_strip()
+{
+ glBegin(GL_QUAD_STRIP);
+ glVertex2f(-1.f, -1.f);
+ glVertex2f(1.f, -1.f);
+ glVertex2f(-0.5f, 0.f);
+ glVertex2f(0.5f, 0.f);
+ glVertex2f(-1.f, 1.f);
+ glVertex2f(1.f, 1.f);
+ glEnd();
+}
+
+void polygon()
+{
+ glBegin(GL_POLYGON);
+ glVertex2f(-1.f, 0.f);
+ glVertex2f(-0.75f, -0.75f);
+ glVertex2f(0.f, -1.f);
+ glVertex2f(0.75f, -0.75f);
+ glVertex2f(1.f, 0.f);
+ glVertex2f(0.75f, 0.75f);
+ glVertex2f(0.f, 1.f);
+ glVertex2f(-0.75f, 0.75f);
+ glEnd();
+}
+
+void prim_test()
+{
+ glPushMatrix();
+ glTranslatef(-6, 1.5f, 0);
+ points();
+ glPopMatrix();
+
+ glPushMatrix();
+ glTranslatef(-3, 1.5f, 0);
+ lines();
+ glPopMatrix();
+
+ glPushMatrix();
+ glTranslatef(0, 1.5f, 0);
+ line_strip();
+ glPopMatrix();
+
+ glPushMatrix();
+ glTranslatef(3, 1.5f, 0);
+ line_loop();
+ glPopMatrix();
+
+ glPushMatrix();
+ glTranslatef(6, 1.5f, 0);
+ triangles();
+ glPopMatrix();
+
+ glPushMatrix();
+ glTranslatef(-6, -1.5f, 0);
+ triangle_strip();
+ glPopMatrix();
+
+ glPushMatrix();
+ glTranslatef(-3, -1.5f, 0);
+ triangle_fan();
+ glPopMatrix();
+
+ glPushMatrix();
+ glTranslatef(0, -1.5f, 0);
+ quads();
+ glPopMatrix();
+
+ glPushMatrix();
+ glTranslatef(3, -1.5f, 0);
+ quad_strip();
+ glPopMatrix();
+
+ glPushMatrix();
+ glTranslatef(6, -1.5f, 0);
+ polygon();
+ glPopMatrix();
+}
+
+void render_primitives(float rotation)
+{
+ rdpq_debug_log_msg("Primitives");
+ glPushMatrix();
+
+ glTranslatef(0, 6, 0);
+ glRotatef(-rotation*2.46f, 0, 1, 0);
+
+ // Configure alpha blending (transparency)
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+ // Set a constant alpha for all vertices
+ glColor4f(1, 1, 1, 0.4f);
+
+ // We want to see back faces as well
+ glDisable(GL_CULL_FACE);
+
+ // Transparent polygons should not write to the depth buffer
+ glDepthMask(GL_FALSE);
+
+ prim_test();
+
+ glDepthMask(GL_TRUE);
+ glEnable(GL_CULL_FACE);
+ glDisable(GL_BLEND);
+
+ glPopMatrix();
+}
+
+#endif
diff --git a/examples/gldemo/skinned.h b/examples/gldemo/skinned.h
new file mode 100644
index 000000000..03d789b9e
--- /dev/null
+++ b/examples/gldemo/skinned.h
@@ -0,0 +1,83 @@
+#ifndef SKINNED_H
+#define SKINNED_H
+
+#include
+#include
+#include
+
+#include "camera.h"
+
+typedef struct {
+ float position[3];
+ float texcoord[2];
+ float normal[3];
+ uint8_t mtx_index;
+} skinned_vertex_t;
+
+static const skinned_vertex_t skinned_vertices[] = {
+ { .position = {-2, 0, -1}, .texcoord = {0.f, 0.f}, .normal = { 0.f, 1.f, 0.f}, .mtx_index = 0 },
+ { .position = {-2, 0, 1}, .texcoord = {1.f, 0.f}, .normal = { 0.f, 1.f, 0.f}, .mtx_index = 0 },
+ { .position = {-1, 0, -1}, .texcoord = {0.f, 1.f}, .normal = { 0.f, 1.f, 0.f}, .mtx_index = 0 },
+ { .position = {-1, 0, 1}, .texcoord = {1.f, 1.f}, .normal = { 0.f, 1.f, 0.f}, .mtx_index = 0 },
+ { .position = { 1, 0, -1}, .texcoord = {0.f, 2.f}, .normal = { 0.f, 1.f, 0.f}, .mtx_index = 1 },
+ { .position = { 1, 0, 1}, .texcoord = {1.f, 2.f}, .normal = { 0.f, 1.f, 0.f}, .mtx_index = 1 },
+ { .position = { 2, 0, -1}, .texcoord = {0.f, 3.f}, .normal = { 0.f, 1.f, 0.f}, .mtx_index = 1 },
+ { .position = { 2, 0, 1}, .texcoord = {1.f, 3.f}, .normal = { 0.f, 1.f, 0.f}, .mtx_index = 1 },
+};
+
+void draw_skinned()
+{
+ glEnable(GL_MATRIX_PALETTE_ARB);
+
+ glEnableClientState(GL_VERTEX_ARRAY);
+ glEnableClientState(GL_TEXTURE_COORD_ARRAY);
+ glEnableClientState(GL_NORMAL_ARRAY);
+ glEnableClientState(GL_MATRIX_INDEX_ARRAY_ARB);
+
+ glVertexPointer( 3, GL_FLOAT, sizeof(skinned_vertex_t), skinned_vertices[0].position);
+ glTexCoordPointer( 2, GL_FLOAT, sizeof(skinned_vertex_t), skinned_vertices[0].texcoord);
+ glNormalPointer( GL_FLOAT, sizeof(skinned_vertex_t), skinned_vertices[0].normal);
+ glMatrixIndexPointerARB(1, GL_UNSIGNED_BYTE, sizeof(skinned_vertex_t), &skinned_vertices[0].mtx_index);
+
+ glDrawArrays(GL_TRIANGLE_STRIP, 0, sizeof(skinned_vertices)/sizeof(skinned_vertex_t));
+
+ glDisableClientState(GL_VERTEX_ARRAY);
+ glDisableClientState(GL_TEXTURE_COORD_ARRAY);
+ glDisableClientState(GL_NORMAL_ARRAY);
+ glDisableClientState(GL_MATRIX_INDEX_ARRAY_ARB);
+
+ glDisable(GL_MATRIX_PALETTE_ARB);
+}
+
+void render_skinned(const camera_t *camera, float animation)
+{
+ rdpq_debug_log_msg("Skinned");
+ glPushMatrix();
+
+ glTranslatef(0, 3, -6);
+ glScalef(2, 2, 2);
+
+ // Set bone transforms. Note that because there is no matrix stack in palette mode, we need
+ // to apply the camera transform and model transform as well for each bone.
+ glMatrixMode(GL_MATRIX_PALETTE_ARB);
+
+ // Set transform of first bone
+ glCurrentPaletteMatrixARB(0);
+ glCopyMatrixN64(GL_MODELVIEW);
+ glRotatef(sinf(animation*0.1f)*45, 0, 0, 1);
+
+ // Set transform of second bone
+ glCurrentPaletteMatrixARB(1);
+ glCopyMatrixN64(GL_MODELVIEW);
+ glRotatef(-sinf(animation*0.1f)*45, 0, 0, 1);
+
+ glMatrixMode(GL_MODELVIEW);
+
+ glDisable(GL_CULL_FACE);
+ draw_skinned();
+ glEnable(GL_CULL_FACE);
+
+ glPopMatrix();
+}
+
+#endif
\ No newline at end of file
diff --git a/examples/gldemo/sphere.h b/examples/gldemo/sphere.h
new file mode 100644
index 000000000..8597a76d3
--- /dev/null
+++ b/examples/gldemo/sphere.h
@@ -0,0 +1,186 @@
+#ifndef SPHERE_H
+#define SPHERE_H
+
+#include
+#include
+#include
+
+#include "vertex.h"
+
+#define SPHERE_RADIUS 20.0f
+#define SPHERE_MIN_RINGS 4
+#define SPHERE_MAX_RINGS 64
+#define SPHERE_MIN_SEGMENTS 4
+#define SPHERE_MAX_SEGMENTS 64
+
+static GLuint sphere_buffers[2];
+static GLuint sphere_array;
+static GLuint sphere_list;
+static uint32_t sphere_rings;
+static uint32_t sphere_segments;
+static uint32_t sphere_vertex_count;
+static uint32_t sphere_index_count;
+
+void setup_sphere()
+{
+ glGenBuffersARB(2, sphere_buffers);
+ sphere_rings = 8;
+ sphere_segments = 8;
+
+ glGenVertexArrays(1, &sphere_array);
+ glBindVertexArray(sphere_array);
+
+ glEnableClientState(GL_VERTEX_ARRAY);
+ glEnableClientState(GL_TEXTURE_COORD_ARRAY);
+ glEnableClientState(GL_NORMAL_ARRAY);
+
+ glBindBufferARB(GL_ARRAY_BUFFER_ARB, sphere_buffers[0]);
+
+ glVertexPointer(3, GL_FLOAT, sizeof(vertex_t), (void*)(0*sizeof(float)));
+ glTexCoordPointer(2, GL_FLOAT, sizeof(vertex_t), (void*)(3*sizeof(float)));
+ glNormalPointer(GL_FLOAT, sizeof(vertex_t), (void*)(5*sizeof(float)));
+
+ glBindBufferARB(GL_ARRAY_BUFFER_ARB, 0);
+
+ glBindVertexArray(0);
+
+ sphere_list = glGenLists(1);
+}
+
+void make_sphere_vertex(vertex_t *dst, uint32_t ring, uint32_t segment)
+{
+ float r = SPHERE_RADIUS;
+ float phi = (M_TWOPI * segment) / sphere_segments;
+ float theta = (M_PI * ring) / (sphere_rings + 1);
+
+ float sintheta = sin(theta);
+
+ float x = r * cosf(phi) * sintheta;
+ float y = r * sinf(phi) * sintheta;
+ float z = r * cosf(theta);
+
+ dst->position[0] = x;
+ dst->position[1] = y;
+ dst->position[2] = z;
+
+ float mag2 = x*x + y*y + z*z;
+ float mag = sqrtf(mag2);
+ float inv_m = 1.0f / mag;
+
+ dst->normal[0] = -x * inv_m;
+ dst->normal[1] = -y * inv_m;
+ dst->normal[2] = -z * inv_m;
+
+ dst->texcoord[0] = segment & 1 ? 1.0f : 0.0f;
+ dst->texcoord[1] = ring & 1 ? 1.0f : 0.0f;
+}
+
+void draw_sphere_internal()
+{
+ glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, sphere_buffers[1]);
+ glBindVertexArray(sphere_array);
+
+ glDrawElements(GL_TRIANGLE_FAN, sphere_segments + 2, GL_UNSIGNED_SHORT, 0);
+ glDrawElements(GL_TRIANGLE_FAN, sphere_segments + 2, GL_UNSIGNED_SHORT, (void*)((sphere_segments + 2) * sizeof(uint16_t)));
+ glDrawElements(GL_TRIANGLES, (sphere_rings - 1) * (sphere_segments * 6), GL_UNSIGNED_SHORT, (void*)((sphere_segments + 2) * 2 * sizeof(uint16_t)));
+
+ glBindVertexArray(0);
+ glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, 0);
+}
+
+void make_sphere_mesh()
+{
+ sphere_vertex_count = sphere_rings * sphere_segments + 2;
+
+ glBindBufferARB(GL_ARRAY_BUFFER_ARB, sphere_buffers[0]);
+ glBufferDataARB(GL_ARRAY_BUFFER_ARB, sphere_vertex_count * sizeof(vertex_t), NULL, GL_STATIC_DRAW_ARB);
+
+ vertex_t *vertices = glMapBufferARB(GL_ARRAY_BUFFER_ARB, GL_WRITE_ONLY_ARB);
+
+ make_sphere_vertex(&vertices[0], 0, 0);
+
+ for (uint32_t r = 0; r < sphere_rings; r++)
+ {
+ for (uint32_t s = 0; s < sphere_segments; s++)
+ {
+ make_sphere_vertex(&vertices[r * sphere_segments + s + 1], r + 1, s);
+ }
+ }
+
+ make_sphere_vertex(&vertices[sphere_vertex_count - 1], sphere_rings + 1, 0);
+
+ glUnmapBufferARB(GL_ARRAY_BUFFER_ARB);
+ glBindBufferARB(GL_ARRAY_BUFFER_ARB, 0);
+
+ uint32_t fan_index_count = sphere_segments + 2;
+ uint32_t ring_index_count = sphere_segments * 6;
+
+ sphere_index_count = fan_index_count * 2 + ring_index_count * (sphere_rings - 1);
+
+ glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, sphere_buffers[1]);
+ glBufferDataARB(GL_ELEMENT_ARRAY_BUFFER_ARB, sphere_index_count * sizeof(uint16_t), NULL, GL_STATIC_DRAW_ARB);
+
+ uint16_t *indices = glMapBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, GL_WRITE_ONLY_ARB);
+
+ // Ends
+ for (uint32_t i = 0; i < fan_index_count - 1; i++)
+ {
+ indices[i] = i;
+ indices[fan_index_count + i] = sphere_vertex_count - i - 1;
+ }
+ indices[sphere_segments + 1] = 1;
+ indices[fan_index_count + sphere_segments + 1] = sphere_vertex_count - 2;
+
+ uint32_t rings_index_offset = fan_index_count * 2;
+
+ // Rings
+ for (uint32_t r = 0; r < sphere_rings - 1; r++)
+ {
+ uint16_t *ring_indices = &indices[rings_index_offset + r * ring_index_count];
+ uint16_t first_ring_index = 1 + r * sphere_segments;
+ uint16_t second_ring_index = 1 + (r + 1) * sphere_segments;
+
+ for (uint32_t s = 0; s < sphere_segments; s++)
+ {
+ uint16_t next_segment = (s + 1) % sphere_segments;
+ ring_indices[s * 6 + 0] = first_ring_index + s;
+ ring_indices[s * 6 + 1] = second_ring_index + s;
+ ring_indices[s * 6 + 2] = first_ring_index + next_segment;
+ ring_indices[s * 6 + 3] = second_ring_index + s;
+ ring_indices[s * 6 + 4] = second_ring_index + next_segment;
+ ring_indices[s * 6 + 5] = first_ring_index + next_segment;
+ }
+ }
+
+ glUnmapBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB);
+ glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, 0);
+
+ glNewList(sphere_list, GL_COMPILE);
+ draw_sphere_internal();
+ glEndList();
+}
+
+void draw_sphere()
+{
+ // This is an example of using display lists
+ glCallList(sphere_list);
+}
+
+void render_sphere(float rotation)
+{
+ rdpq_debug_log_msg("Sphere");
+ glPushMatrix();
+
+ glRotatef(rotation*0.23f, 1, 0, 0);
+ glRotatef(rotation*0.98f, 0, 0, 1);
+ glRotatef(rotation*1.71f, 0, 1, 0);
+
+ // We want to see back faces instead of front faces, because the camera will be inside the sphere
+ glCullFace(GL_FRONT);
+ draw_sphere();
+ glCullFace(GL_BACK);
+
+ glPopMatrix();
+}
+
+#endif
diff --git a/examples/gldemo/vertex.h b/examples/gldemo/vertex.h
new file mode 100644
index 000000000..d2cdb98ed
--- /dev/null
+++ b/examples/gldemo/vertex.h
@@ -0,0 +1,13 @@
+#ifndef VERTEX
+#define VERTEX
+
+#include
+
+typedef struct {
+ float position[3];
+ float texcoord[2];
+ float normal[3];
+ uint32_t color;
+} vertex_t;
+
+#endif
diff --git a/examples/joypadtest/Makefile b/examples/joypadtest/Makefile
index 2d984a017..abc8095e6 100644
--- a/examples/joypadtest/Makefile
+++ b/examples/joypadtest/Makefile
@@ -2,6 +2,8 @@ all: joypadtest.z64
.PHONY: all
BUILD_DIR = build
+# This example uses cpak, which is a preview API
+LIBDRAGON_PREVIEW = 2
include $(N64_INST)/include/n64.mk
OBJS = $(BUILD_DIR)/joypadtest.o
diff --git a/examples/joypadtest/joypadtest.c b/examples/joypadtest/joypadtest.c
index 4d61618f3..c5ce103ac 100644
--- a/examples/joypadtest/joypadtest.c
+++ b/examples/joypadtest/joypadtest.c
@@ -29,19 +29,19 @@ const char *format_joypad_accessory_type(joypad_accessory_type_t accessory_type)
switch (accessory_type)
{
case JOYPAD_ACCESSORY_TYPE_NONE:
- return "None ";
+ return "None ";
case JOYPAD_ACCESSORY_TYPE_CONTROLLER_PAK:
- return "Memory ";
+ return "Controller Pak ";
case JOYPAD_ACCESSORY_TYPE_RUMBLE_PAK:
- return "Rumble Pak ";
+ return "Rumble Pak ";
case JOYPAD_ACCESSORY_TYPE_TRANSFER_PAK:
- return "Transfer Pak";
+ return "Transfer Pak ";
case JOYPAD_ACCESSORY_TYPE_BIO_SENSOR:
- return "Bio Sensor ";
+ return "Bio Sensor ";
case JOYPAD_ACCESSORY_TYPE_SNAP_STATION:
- return "Snap Station";
+ return "Snap Station ";
default:
- return "Unknown ";
+ return "Unknown ";
}
}
@@ -80,16 +80,17 @@ int main(void)
{
joypad_style_t style;
joypad_accessory_type_t accessory_type;
- bool rumble_supported;
- bool rumble_active;
joypad_inputs_t inputs;
+ bool rumble_supported, rumble_active;
+ int cpak_num_banks[4] = {0, 0, 0, 0};
+ int b_hold_time[4] = {0, 0, 0, 0};
- timer_init();
- joypad_init();
debug_init_emulog();
+ debug_init_usblog();
console_init();
console_set_render_mode(RENDER_MANUAL);
console_set_debug(false);
+ joypad_init();
while (1)
{
@@ -98,7 +99,7 @@ int main(void)
printf("\n");
printf("LibDragon Joypad Subsystem Test\n");
printf("Hold A to test rumble motors.\n");
- printf("Press B to trigger an exception.\n");
+ printf("Hold B to trigger the exception screen.\n");
printf("\n");
joypad_poll();
@@ -107,12 +108,15 @@ int main(void)
{
style = joypad_get_style(port);
accessory_type = joypad_get_accessory_type(port);
- rumble_supported = joypad_get_rumble_supported(port);
- rumble_active = joypad_get_rumble_active(port);
inputs = joypad_get_inputs(port);
+ rumble_supported = joypad_get_rumble_supported(port);
if (rumble_supported)
{
+ // NOTE: Rumble is suported on both N64 and GameCube controllers
+ // Checking for JOYPAD_ACCESSORY_TYPE_RUMBLE_PAK would only cover
+ // the N64 controller, as GameCube controllers do not have accessories.
+ rumble_active = joypad_get_rumble_active(port);
if (inputs.btn.a && !rumble_active)
{
joypad_set_rumble_active(port, true);
@@ -122,16 +126,46 @@ int main(void)
joypad_set_rumble_active(port, false);
}
}
+
+ if (accessory_type == JOYPAD_ACCESSORY_TYPE_CONTROLLER_PAK)
+ {
+ if (cpak_num_banks[port] == 0)
+ {
+ cpak_num_banks[port] = cpak_probe_banks(port);
+ }
+ }
+ else
+ {
+ cpak_num_banks[port] = 0; // Reset bank count for non-Controller Paks
+ }
if (inputs.btn.b)
{
- debugf((char *)0x1);
+ if (!b_hold_time[port])
+ b_hold_time[port] = get_ticks_ms();
+ if (get_ticks_ms() - b_hold_time[port] > 1000)
+ {
+ assertf(0, "B button held for one second, exception screen triggered for debugging purposes");
+ }
+ }
+ else
+ {
+ b_hold_time[port] = 0;
}
printf("Port %d ", port + 1);
printf("Style: %s ", format_joypad_style(style));
printf("Pak: %s ", format_joypad_accessory_type(accessory_type));
- printf("Rumble: %s", format_joypad_rumble(rumble_supported, rumble_active));
+ if (rumble_supported)
+ {
+ printf("Rumble: %s", format_joypad_rumble(rumble_supported, rumble_active));
+ }
+ else if (accessory_type == JOYPAD_ACCESSORY_TYPE_CONTROLLER_PAK)
+ {
+ printf("Banks: %d", cpak_num_banks[port]);
+ if (!cpak_supports_bankswitching(port))
+ printf(" (no b/s)");
+ }
printf("\n");
print_joypad_inputs(inputs);
printf("\n");
diff --git a/examples/loadspritefromsd/.gitignore b/examples/loadspritefromsd/.gitignore
new file mode 100644
index 000000000..87ef66815
--- /dev/null
+++ b/examples/loadspritefromsd/.gitignore
@@ -0,0 +1 @@
+filesystem/
\ No newline at end of file
diff --git a/examples/loadspritefromsd/loadspritefromsd.c b/examples/loadspritefromsd/loadspritefromsd.c
index af1a15f44..59f7ac23f 100644
--- a/examples/loadspritefromsd/loadspritefromsd.c
+++ b/examples/loadspritefromsd/loadspritefromsd.c
@@ -39,7 +39,7 @@ void load_sprite(int id) {
int main(void) {
/* Initialize peripherals */
- display_init(RESOLUTION_320x240, DEPTH_16_BPP, 2, GAMMA_NONE, FILTERS_RESAMPLE);
+ display_init(RESOLUTION_320x240, DEPTH_16_BPP, 2, GAMMA_NONE, FILTERS_DISABLED);
dfs_init(DFS_DEFAULT_LOCATION);
joypad_init();
diff --git a/examples/lossysprite/.gitignore b/examples/lossysprite/.gitignore
new file mode 100644
index 000000000..095b62d62
--- /dev/null
+++ b/examples/lossysprite/.gitignore
@@ -0,0 +1,5 @@
+build/
+filesystem/
+*.z64
+*.pak
+*.sav
diff --git a/examples/lossysprite/Makefile b/examples/lossysprite/Makefile
new file mode 100644
index 000000000..fbd4d86fe
--- /dev/null
+++ b/examples/lossysprite/Makefile
@@ -0,0 +1,60 @@
+BUILD_DIR=build
+# This example uses BC1Q and H264I, which are preview APIs
+LIBDRAGON_PREVIEW = 2
+include $(N64_INST)/include/n64.mk
+
+src = lossysprite.c
+
+BG_INDICES = 0 1 2 3 4 5 6
+RAW_SPRITES = $(foreach i,$(BG_INDICES),filesystem/bg$(i)_raw.sprite)
+BC1Q_SPRITES = $(foreach i,$(BG_INDICES),filesystem/bg$(i)_bc1q.sprite)
+H264I_SPRITES = $(foreach i,$(BG_INDICES),filesystem/bg$(i)_h264i.sprite)
+
+assets_conv = \
+ $(RAW_SPRITES) \
+ $(BC1Q_SPRITES) \
+ $(H264I_SPRITES) \
+ filesystem/n64brew.sprite
+
+all: lossysprite.z64
+
+# Each bgN.png is encoded three ways so the demo can switch formats at runtime.
+# mksprite -o takes a directory and names the output after the input file, so
+# each rule uses a per-format scratch dir to avoid racing on bg$*.sprite when
+# make runs the rules in parallel, then renames into filesystem/.
+filesystem/bg%_raw.sprite: assets/bg%.png
+ @mkdir -p $(dir $@) $(BUILD_DIR)/sprites_raw
+ @echo " [SPRITE] $@"
+ $(N64_MKSPRITE) --format RGBA16 --compress 1 -o $(BUILD_DIR)/sprites_raw "$<"
+ mv $(BUILD_DIR)/sprites_raw/bg$*.sprite "$@"
+
+filesystem/bg%_bc1q.sprite: assets/bg%.png
+ @mkdir -p $(dir $@) $(BUILD_DIR)/sprites_bc1q
+ @echo " [SPRITE] $@"
+ $(N64_MKSPRITE) --format RGBA16 --lossy 75 --compress 1 -o $(BUILD_DIR)/sprites_bc1q "$<"
+ mv $(BUILD_DIR)/sprites_bc1q/bg$*.sprite "$@"
+
+filesystem/bg%_h264i.sprite: assets/bg%.png
+ @mkdir -p $(dir $@) $(BUILD_DIR)/sprites_h264i
+ @echo " [SPRITE] $@"
+ $(N64_MKSPRITE) --format RGBA16 --lossy 75 --compress 3 -o $(BUILD_DIR)/sprites_h264i "$<"
+ mv $(BUILD_DIR)/sprites_h264i/bg$*.sprite "$@"
+
+# Brew sprite in standard RGBA16 format (losslessly compressed)
+filesystem/n64brew.sprite: assets/n64brew.png
+ @mkdir -p $(dir $@)
+ @echo " [SPRITE] $@"
+ $(N64_MKSPRITE) --format RGBA16 --compress -o filesystem "$<"
+
+$(BUILD_DIR)/lossysprite.dfs: $(assets_conv)
+$(BUILD_DIR)/lossysprite.elf: $(src:%.c=$(BUILD_DIR)/%.o)
+
+lossysprite.z64: N64_ROM_TITLE="Lossy Sprite Demo"
+lossysprite.z64: $(BUILD_DIR)/lossysprite.dfs
+
+clean:
+ rm -rf $(BUILD_DIR) filesystem lossysprite.z64
+
+-include $(wildcard $(BUILD_DIR)/*.d)
+
+.PHONY: all clean
diff --git a/examples/lossysprite/assets/bg0.png b/examples/lossysprite/assets/bg0.png
new file mode 100644
index 000000000..27ed6c6f6
Binary files /dev/null and b/examples/lossysprite/assets/bg0.png differ
diff --git a/examples/lossysprite/assets/bg1.png b/examples/lossysprite/assets/bg1.png
new file mode 100644
index 000000000..70ea18b24
Binary files /dev/null and b/examples/lossysprite/assets/bg1.png differ
diff --git a/examples/lossysprite/assets/bg2.png b/examples/lossysprite/assets/bg2.png
new file mode 100644
index 000000000..4e12068b9
Binary files /dev/null and b/examples/lossysprite/assets/bg2.png differ
diff --git a/examples/lossysprite/assets/bg3.png b/examples/lossysprite/assets/bg3.png
new file mode 100644
index 000000000..ee3cfbc48
Binary files /dev/null and b/examples/lossysprite/assets/bg3.png differ
diff --git a/examples/lossysprite/assets/bg4.png b/examples/lossysprite/assets/bg4.png
new file mode 100644
index 000000000..e0394548b
Binary files /dev/null and b/examples/lossysprite/assets/bg4.png differ
diff --git a/examples/lossysprite/assets/bg5.png b/examples/lossysprite/assets/bg5.png
new file mode 100644
index 000000000..d70e86686
Binary files /dev/null and b/examples/lossysprite/assets/bg5.png differ
diff --git a/examples/lossysprite/assets/bg6.png b/examples/lossysprite/assets/bg6.png
new file mode 100644
index 000000000..e6a3f2f9c
Binary files /dev/null and b/examples/lossysprite/assets/bg6.png differ
diff --git a/examples/lossysprite/assets/n64brew.png b/examples/lossysprite/assets/n64brew.png
new file mode 100644
index 000000000..106eb6b4a
Binary files /dev/null and b/examples/lossysprite/assets/n64brew.png differ
diff --git a/examples/lossysprite/lossysprite.c b/examples/lossysprite/lossysprite.c
new file mode 100644
index 000000000..e06443312
--- /dev/null
+++ b/examples/lossysprite/lossysprite.c
@@ -0,0 +1,121 @@
+#include
+
+typedef enum {
+ FMT_RAW = 0,
+ FMT_BC1Q = 1,
+ FMT_H264I = 2,
+ FMT_COUNT
+} fmt_t;
+
+static const char *fmt_name[FMT_COUNT] = { "LOSSLESS", "BC1Q", "H264I" };
+static const char *fmt_suffix[FMT_COUNT] = { "raw", "bc1q", "h264i" };
+
+#define BG_COUNT 7
+#define FONT_ID 1
+
+static int bg_idx = 0;
+static fmt_t fmt_idx = FMT_BC1Q;
+static sprite_t *bg_sprite = NULL;
+static uint64_t last_load_us = 0;
+static size_t last_encoded_bytes = 0;
+static size_t last_raw_bytes = 0;
+
+static void load_bg(void) {
+ if (bg_sprite) sprite_free(bg_sprite);
+ char fn[64];
+ snprintf(fn, sizeof(fn), "rom:/bg%d_%s.sprite", bg_idx, fmt_suffix[fmt_idx]);
+
+ // Stat the on-disk size so the corner readout can show a compression
+ // ratio relative to the uncompressed RGBA16 footprint.
+ last_encoded_bytes = 0;
+ FILE *f = fopen(fn, "rb");
+ if (f) {
+ fseek(f, 0, SEEK_END);
+ last_encoded_bytes = (size_t)ftell(f);
+ fclose(f);
+ }
+
+ uint64_t t0 = get_ticks_us();
+ bg_sprite = sprite_load(fn);
+ last_load_us = get_ticks_us() - t0;
+ last_raw_bytes = (size_t)bg_sprite->width * bg_sprite->height * 2;
+
+ float pct = last_raw_bytes
+ ? 100.0f * (float)last_encoded_bytes / (float)last_raw_bytes : 0.0f;
+ debugf("Loaded %s [%s] %u B (%.1f%% of raw) in %llu us\n",
+ fn, fmt_name[fmt_idx], (unsigned)last_encoded_bytes, pct, last_load_us);
+}
+
+int main(void)
+{
+ debug_init_emulog();
+ debug_init_usblog();
+ display_init(RESOLUTION_640x480, DEPTH_16_BPP, 2, GAMMA_NONE, FILTERS_DISABLED);
+ dfs_init(DFS_DEFAULT_LOCATION);
+ rdpq_init();
+ joypad_init();
+
+ // Register the Level 3 (H264I) and Level 1 (BC1Q) lossy-sprite decoders
+ lspr3_init();
+ lspr1_init();
+
+ // Font for the on-screen codec/timing readout
+ rdpq_text_register_font(FONT_ID, rdpq_font_load_builtin(FONT_BUILTIN_DEBUG_MONO));
+
+ // Load the initial background sprite
+ load_bg();
+
+ // Load the losslessly-compressed brew sprite
+ sprite_t *brew_sprite = sprite_load("rom:/n64brew.sprite");
+
+ const int sw = display_get_width();
+ const int sh = display_get_height();
+ const int bw = brew_sprite->width;
+ const int bh = brew_sprite->height;
+
+ float x = 100.0f, y = 80.0f;
+ float vx = 2.5f, vy = 2.0f;
+
+ while (1) {
+ // Bounce the brew sprite around the screen edges
+ x += vx; y += vy;
+ if (x < 0) { x = 0; vx = -vx; }
+ if (y < 0) { y = 0; vy = -vy; }
+ if (x + bw > sw) { x = sw - bw; vx = -vx; }
+ if (y + bh > sh) { y = sh - bh; vy = -vy; }
+
+ surface_t *screen = display_get();
+ rdpq_attach(screen, NULL);
+ {
+ rdpq_set_mode_standard();
+ // Draw the background
+ rdpq_sprite_blit(bg_sprite, 0, 0, NULL);
+ // Draw the brew sprite
+ rdpq_mode_alphacompare(1); // colorkey (draw pixel with alpha >= 1)
+ rdpq_sprite_blit(brew_sprite, x, y, NULL);
+
+ // Codec readout in the top-left corner: shows active format,
+ // last decode time, encoded size, and encoded-as-percent of
+ // the uncompressed RGBA16 footprint.
+ float pct = last_raw_bytes
+ ? 100.0f * (float)last_encoded_bytes / (float)last_raw_bytes : 0.0f;
+ rdpq_text_printf(NULL, FONT_ID, 12, 20,
+ "bg%d %s\n%u B %.1f%%\n%llu us",
+ bg_idx, fmt_name[fmt_idx],
+ (unsigned)last_encoded_bytes, pct,
+ last_load_us
+ );
+ }
+ rdpq_detach_show();
+
+ // L/R or A/B cycle backgrounds; C-Left/Right cycle codecs.
+ joypad_poll();
+ joypad_buttons_t btn = joypad_get_buttons_pressed(JOYPAD_PORT_1);
+ bool reload = false;
+ if (btn.r || btn.a) { bg_idx = (bg_idx + 1) % BG_COUNT; reload = true; }
+ if (btn.l || btn.z || btn.b){ bg_idx = (bg_idx - 1 + BG_COUNT) % BG_COUNT; reload = true; }
+ if (btn.c_right) { fmt_idx = (fmt_idx + 1) % FMT_COUNT; reload = true; }
+ if (btn.c_left) { fmt_idx = (fmt_idx - 1 + FMT_COUNT) % FMT_COUNT; reload = true; }
+ if (reload) load_bg();
+ }
+}
diff --git a/examples/meshviewer/.gitignore b/examples/meshviewer/.gitignore
new file mode 100644
index 000000000..01fa9123c
--- /dev/null
+++ b/examples/meshviewer/.gitignore
@@ -0,0 +1,2 @@
+filesystem/
+*.pak
\ No newline at end of file
diff --git a/examples/meshviewer/Makefile b/examples/meshviewer/Makefile
new file mode 100644
index 000000000..a9720fd41
--- /dev/null
+++ b/examples/meshviewer/Makefile
@@ -0,0 +1,55 @@
+BUILD_DIR=build
+# This example uses OpenGL and model64, which are preview APIs
+LIBDRAGON_PREVIEW = 2
+include $(N64_INST)/include/n64.mk
+
+src = meshviewer.c
+
+assets_glb = $(wildcard assets/*.glb)
+assets_gltf = $(wildcard assets/*.gltf)
+assets_png = $(wildcard assets/*.png)
+
+assets_models_glb = $(addprefix filesystem/models/,$(notdir $(assets_glb:%.glb=%.model64)))
+assets_models_gltf = $(addprefix filesystem/models/,$(notdir $(assets_gltf:%.gltf=%.model64)))
+assets_models = $(assets_models_glb) $(assets_models_gltf)
+
+assets_anims_glb = $(addprefix filesystem/models/,$(notdir $(assets_glb:%.glb=%.anims)))
+assets_anims_gltf = $(addprefix filesystem/models/,$(notdir $(assets_gltf:%.gltf=%.anims)))
+assets_anims = $(assets_anims_glb) $(assets_anims_gltf)
+
+assets_sprites = $(addprefix filesystem/models/,$(notdir $(assets_png:assets/%.png=%.sprite)))
+
+all: meshviewer.z64
+
+filesystem/models/%.model64: assets/%.glb
+ @mkdir -p $(dir $@)
+ @echo " [MODEL] $@"
+ $(N64_MKMODEL) -v --anim-no-stream --compress 0 -o "$(dir $@)" "$<" 2>&1 \
+ | sed -n 's/^Converting animation //p' >"$(@:%.model64=%.anims)";
+
+filesystem/models/%.model64: assets/%.gltf assets/%.bin
+ @mkdir -p $(dir $@)
+ @echo " [MODEL] $@"
+ $(N64_MKMODEL) -v --anim-no-stream --compress 0 -o "$(dir $@)" "$<" 2>&1 \
+ | sed -n 's/^Converting animation //p' >"$(@:%.model64=%.anims)";
+
+filesystem/models/%.anims: filesystem/models/%.model64
+ @true
+
+filesystem/models/%.sprite: assets/%.png
+ @mkdir -p $(dir $@)
+ @echo " [SPRITE] $@"
+ @$(N64_MKSPRITE) -f RGBA16 -o "$(dir $@)" "$<"
+
+$(BUILD_DIR)/meshviewer.dfs: $(assets_models) $(assets_anims) $(assets_sprites)
+$(BUILD_DIR)/meshviewer.elf: $(src:%.c=$(BUILD_DIR)/%.o)
+
+meshviewer.z64: N64_ROM_TITLE="Mesh Viewer"
+meshviewer.z64: $(BUILD_DIR)/meshviewer.dfs
+
+clean:
+ rm -rf $(BUILD_DIR) filesystem/ meshviewer.z64
+
+-include $(wildcard $(BUILD_DIR)/*.d)
+
+.PHONY: all clean
diff --git a/examples/meshviewer/assets/.gitignore b/examples/meshviewer/assets/.gitignore
new file mode 100644
index 000000000..406c42106
--- /dev/null
+++ b/examples/meshviewer/assets/.gitignore
@@ -0,0 +1,2 @@
+# *.bin file is part of the gltf file
+!*.bin
\ No newline at end of file
diff --git a/examples/meshviewer/assets/animated_cube.glb b/examples/meshviewer/assets/animated_cube.glb
new file mode 100644
index 000000000..48c47bb9d
Binary files /dev/null and b/examples/meshviewer/assets/animated_cube.glb differ
diff --git a/examples/meshviewer/assets/animated_cylinder.glb b/examples/meshviewer/assets/animated_cylinder.glb
new file mode 100644
index 000000000..525f903f5
Binary files /dev/null and b/examples/meshviewer/assets/animated_cylinder.glb differ
diff --git a/examples/meshviewer/assets/animated_pyramid.glb b/examples/meshviewer/assets/animated_pyramid.glb
new file mode 100644
index 000000000..24296ef33
Binary files /dev/null and b/examples/meshviewer/assets/animated_pyramid.glb differ
diff --git a/examples/meshviewer/assets/coffeecup-texture.png b/examples/meshviewer/assets/coffeecup-texture.png
new file mode 100644
index 000000000..7bda10919
Binary files /dev/null and b/examples/meshviewer/assets/coffeecup-texture.png differ
diff --git a/examples/meshviewer/assets/coffeecup.bin b/examples/meshviewer/assets/coffeecup.bin
new file mode 100644
index 000000000..593b65e88
Binary files /dev/null and b/examples/meshviewer/assets/coffeecup.bin differ
diff --git a/examples/meshviewer/assets/coffeecup.gltf b/examples/meshviewer/assets/coffeecup.gltf
new file mode 100644
index 000000000..10b94b46c
--- /dev/null
+++ b/examples/meshviewer/assets/coffeecup.gltf
@@ -0,0 +1,348 @@
+{
+ "asset":{
+ "generator":"Khronos glTF Blender I/O v4.5.47",
+ "version":"2.0"
+ },
+ "scene":0,
+ "scenes":[
+ {
+ "name":"Scene",
+ "nodes":[
+ 0
+ ]
+ }
+ ],
+ "nodes":[
+ {
+ "mesh":0,
+ "name":"Cylinder"
+ }
+ ],
+ "animations":[
+ {
+ "channels":[
+ {
+ "sampler":0,
+ "target":{
+ "node":0,
+ "path":"translation"
+ }
+ },
+ {
+ "sampler":1,
+ "target":{
+ "node":0,
+ "path":"rotation"
+ }
+ },
+ {
+ "sampler":2,
+ "target":{
+ "node":0,
+ "path":"scale"
+ }
+ }
+ ],
+ "name":"Pulse",
+ "samplers":[
+ {
+ "input":4,
+ "interpolation":"STEP",
+ "output":5
+ },
+ {
+ "input":4,
+ "interpolation":"STEP",
+ "output":6
+ },
+ {
+ "input":7,
+ "interpolation":"LINEAR",
+ "output":8
+ }
+ ]
+ },
+ {
+ "channels":[
+ {
+ "sampler":0,
+ "target":{
+ "node":0,
+ "path":"translation"
+ }
+ },
+ {
+ "sampler":1,
+ "target":{
+ "node":0,
+ "path":"rotation"
+ }
+ },
+ {
+ "sampler":2,
+ "target":{
+ "node":0,
+ "path":"scale"
+ }
+ }
+ ],
+ "name":"Slide",
+ "samplers":[
+ {
+ "input":9,
+ "interpolation":"LINEAR",
+ "output":10
+ },
+ {
+ "input":11,
+ "interpolation":"STEP",
+ "output":6
+ },
+ {
+ "input":11,
+ "interpolation":"STEP",
+ "output":12
+ }
+ ]
+ }
+ ],
+ "materials":[
+ {
+ "doubleSided":true,
+ "name":"Material",
+ "pbrMetallicRoughness":{
+ "baseColorTexture":{
+ "index":0
+ },
+ "metallicFactor":0,
+ "roughnessFactor":0.5
+ }
+ }
+ ],
+ "meshes":[
+ {
+ "name":"Cylinder",
+ "primitives":[
+ {
+ "attributes":{
+ "POSITION":0,
+ "NORMAL":1,
+ "TEXCOORD_0":2
+ },
+ "indices":3,
+ "material":0
+ }
+ ]
+ }
+ ],
+ "textures":[
+ {
+ "sampler":0,
+ "source":0
+ }
+ ],
+ "images":[
+ {
+ "mimeType":"image/png",
+ "name":"coffeecup-texture",
+ "uri":"coffeecup-texture.png"
+ }
+ ],
+ "accessors":[
+ {
+ "bufferView":0,
+ "componentType":5126,
+ "count":412,
+ "max":[
+ 0.5,
+ 0.5,
+ 0.5
+ ],
+ "min":[
+ -0.905262291431427,
+ -0.5,
+ -0.5
+ ],
+ "type":"VEC3"
+ },
+ {
+ "bufferView":1,
+ "componentType":5126,
+ "count":412,
+ "type":"VEC3"
+ },
+ {
+ "bufferView":2,
+ "componentType":5126,
+ "count":412,
+ "type":"VEC2"
+ },
+ {
+ "bufferView":3,
+ "componentType":5123,
+ "count":792,
+ "type":"SCALAR"
+ },
+ {
+ "bufferView":4,
+ "componentType":5126,
+ "count":2,
+ "max":[
+ 1.0833333333333333
+ ],
+ "min":[
+ 0
+ ],
+ "type":"SCALAR"
+ },
+ {
+ "bufferView":5,
+ "componentType":5126,
+ "count":2,
+ "type":"VEC3"
+ },
+ {
+ "bufferView":6,
+ "componentType":5126,
+ "count":2,
+ "type":"VEC4"
+ },
+ {
+ "bufferView":7,
+ "componentType":5126,
+ "count":27,
+ "max":[
+ 1.0833333333333333
+ ],
+ "min":[
+ 0
+ ],
+ "type":"SCALAR"
+ },
+ {
+ "bufferView":8,
+ "componentType":5126,
+ "count":27,
+ "type":"VEC3"
+ },
+ {
+ "bufferView":9,
+ "componentType":5126,
+ "count":25,
+ "max":[
+ 1
+ ],
+ "min":[
+ 0
+ ],
+ "type":"SCALAR"
+ },
+ {
+ "bufferView":10,
+ "componentType":5126,
+ "count":25,
+ "type":"VEC3"
+ },
+ {
+ "bufferView":11,
+ "componentType":5126,
+ "count":2,
+ "max":[
+ 1
+ ],
+ "min":[
+ 0
+ ],
+ "type":"SCALAR"
+ },
+ {
+ "bufferView":12,
+ "componentType":5126,
+ "count":2,
+ "type":"VEC3"
+ }
+ ],
+ "bufferViews":[
+ {
+ "buffer":0,
+ "byteLength":4944,
+ "byteOffset":0,
+ "target":34962
+ },
+ {
+ "buffer":0,
+ "byteLength":4944,
+ "byteOffset":4944,
+ "target":34962
+ },
+ {
+ "buffer":0,
+ "byteLength":3296,
+ "byteOffset":9888,
+ "target":34962
+ },
+ {
+ "buffer":0,
+ "byteLength":1584,
+ "byteOffset":13184,
+ "target":34963
+ },
+ {
+ "buffer":0,
+ "byteLength":8,
+ "byteOffset":14768
+ },
+ {
+ "buffer":0,
+ "byteLength":24,
+ "byteOffset":14776
+ },
+ {
+ "buffer":0,
+ "byteLength":32,
+ "byteOffset":14800
+ },
+ {
+ "buffer":0,
+ "byteLength":108,
+ "byteOffset":14832
+ },
+ {
+ "buffer":0,
+ "byteLength":324,
+ "byteOffset":14940
+ },
+ {
+ "buffer":0,
+ "byteLength":100,
+ "byteOffset":15264
+ },
+ {
+ "buffer":0,
+ "byteLength":300,
+ "byteOffset":15364
+ },
+ {
+ "buffer":0,
+ "byteLength":8,
+ "byteOffset":15664
+ },
+ {
+ "buffer":0,
+ "byteLength":24,
+ "byteOffset":15672
+ }
+ ],
+ "samplers":[
+ {
+ "magFilter":9729,
+ "minFilter":9987
+ }
+ ],
+ "buffers":[
+ {
+ "byteLength":15696,
+ "uri":"coffeecup.bin"
+ }
+ ]
+}
diff --git a/examples/meshviewer/meshviewer.c b/examples/meshviewer/meshviewer.c
new file mode 100644
index 000000000..2353b29be
--- /dev/null
+++ b/examples/meshviewer/meshviewer.c
@@ -0,0 +1,448 @@
+#include
+#include
+#include
+#include
+
+// Limits to avoid dynamic allocation
+#define MAX_MODELS 128
+#define MAX_MODEL_NAME 256
+#define MAX_MODEL_PATH 512
+#define MAX_ANIMS 64
+
+typedef struct
+{
+ char name[MAX_MODEL_NAME];
+ char path[MAX_MODEL_PATH];
+} model_entry_t;
+
+typedef struct
+{
+ model_entry_t entries[MAX_MODELS];
+ int count;
+} model_list_t;
+
+static model64_t *model = NULL;
+static int model_index = 0;
+static int anim_index = 0;
+static bool sd_available = false;
+static bool active_is_sd = false;
+static bool tint_mesh = true;
+
+// Optional Passes
+static bool use_shadowpass = false;
+static bool use_outlinepass = false;
+
+static char anim_name_buf[MAX_ANIMS][MAX_MODEL_NAME];
+static const char *anim_names[MAX_ANIMS] = {0};
+static int anim_count = 0;
+
+typedef enum
+{
+ MODEL_SOURCE_ROM = 0,
+ MODEL_SOURCE_SD = 1,
+} model_source_t;
+
+static model_source_t current_source = MODEL_SOURCE_ROM;
+static model_list_t rom_models = {0};
+static model_list_t sd_models = {0};
+
+static const int font_id = 10;
+
+static const GLfloat light_ambient[] = {0.12f, 0.12f, 0.12f, 1.0f};
+static const GLfloat key_diffuse[] = {1.00f, 0.95f, 0.85f, 1.0f};
+static const GLfloat fill_diffuse[] = {0.35f, 0.40f, 0.50f, 1.0f};
+static const GLfloat rim_diffuse[] = {0.60f, 0.60f, 0.70f, 1.0f};
+
+static void setup_lighting(void)
+{
+ glLightModelfv(GL_LIGHT_MODEL_AMBIENT, light_ambient);
+ glEnable(GL_LIGHTING);
+ glEnable(GL_LIGHT0);
+ glEnable(GL_LIGHT1);
+ glEnable(GL_LIGHT2);
+
+ glLightfv(GL_LIGHT0, GL_DIFFUSE, key_diffuse);
+ glLightfv(GL_LIGHT1, GL_DIFFUSE, fill_diffuse);
+ glLightfv(GL_LIGHT2, GL_DIFFUSE, rim_diffuse);
+
+ glEnable(GL_NORMALIZE);
+ glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE);
+ glEnable(GL_COLOR_MATERIAL);
+}
+
+static void update_light_positions(void)
+{
+ static const GLfloat key_pos[] = {2.5f, 2.0f, 2.5f, 1.0f};
+ static const GLfloat fill_pos[] = {-2.5f, 1.0f, 2.0f, 1.0f};
+ static const GLfloat rim_pos[] = {0.0f, 2.5f, -2.5f, 1.0f};
+
+ glLightfv(GL_LIGHT0, GL_POSITION, key_pos);
+ glLightfv(GL_LIGHT1, GL_POSITION, fill_pos);
+ glLightfv(GL_LIGHT2, GL_POSITION, rim_pos);
+}
+
+static bool str_ends_with(const char *str, const char *suffix)
+{
+ size_t str_len = strlen(str);
+ size_t suffix_len = strlen(suffix);
+ if (suffix_len > str_len)
+ return false;
+ return strcmp(str + str_len - suffix_len, suffix) == 0;
+}
+
+static void model_list_add(model_list_t *list, const char *path,
+ const char *name)
+{
+ if (!list || list->count >= MAX_MODELS)
+ return;
+
+ model_entry_t *entry = &list->entries[list->count];
+ snprintf(entry->path, sizeof(entry->path), "%s", path);
+ snprintf(entry->name, sizeof(entry->name), "%s", name);
+ list->count++;
+}
+
+static void scan_models(const char *base_path, model_list_t *list)
+{
+ if (list) {
+ list->count = 0;
+ }
+
+ dir_t dir = {0};
+ if (dir_findfirst(base_path, &dir) != 0)
+ return;
+
+ do {
+ if (dir.d_type != DT_REG)
+ continue;
+ if (!str_ends_with(dir.d_name, ".model64"))
+ continue;
+
+ char full_path[MAX_MODEL_PATH];
+ snprintf(full_path, sizeof(full_path), "%s/%s", base_path, dir.d_name);
+
+ char name_buf[MAX_MODEL_NAME];
+ strncpy(name_buf, dir.d_name, sizeof(name_buf) - 1);
+ name_buf[sizeof(name_buf) - 1] = '\0';
+ char *ext = strstr(name_buf, ".model64");
+ if (ext)
+ *ext = '\0';
+
+ model_list_add(list, full_path, name_buf);
+ } while (dir_findnext(base_path, &dir) == 0);
+}
+
+static void reset_root_transform(void)
+{
+ if (!model)
+ return;
+ model64_node_t *node = model64_get_node(model, 0);
+ model64_set_node_pos(model, node, 0.0f, 0.0f, 0.0f);
+ model64_set_node_scale(model, node, 1.0f, 1.0f, 1.0f);
+ model64_set_node_rot_quat(model, node, 0.0f, 0.0f, 0.0f, 1.0f);
+}
+
+static void build_anim_list(const char *model_path)
+{
+ anim_count = 0;
+ memset(anim_name_buf, 0, sizeof(anim_name_buf));
+ memset((void *)anim_names, 0, sizeof(anim_names));
+
+ if (!model_path || !model_path[0])
+ return;
+
+ char anims_path[MAX_MODEL_PATH];
+ snprintf(anims_path, sizeof(anims_path), "%s", model_path);
+
+ char *ext = strrchr(anims_path, '.');
+ if (!ext)
+ return;
+ snprintf(ext, anims_path + sizeof(anims_path) - ext, ".anims");
+
+ FILE *fp = fopen(anims_path, "r");
+ if (!fp)
+ return;
+
+ char line[MAX_MODEL_NAME];
+ while (anim_count < MAX_ANIMS && fgets(line, sizeof(line), fp)) {
+ size_t end = strcspn(line, "\r\n");
+ line[end] = '\0';
+ if (line[0] == '\0')
+ continue;
+ snprintf(anim_name_buf[anim_count], sizeof(anim_name_buf[anim_count]),
+ "%s", line);
+ anim_names[anim_count] = anim_name_buf[anim_count];
+ anim_count++;
+ }
+
+ fclose(fp);
+}
+
+static void play_current_anim(void)
+{
+ if (!model)
+ return;
+ if (anim_count <= 0)
+ return;
+ if (anim_index < 0 || anim_index >= anim_count)
+ anim_index = 0;
+ const char *anim = anim_names[anim_index];
+ if (!anim)
+ return;
+ reset_root_transform();
+ model64_anim_play(model, anim, MODEL64_ANIM_SLOT_0, false, 0.0f);
+ model64_anim_set_loop(model, MODEL64_ANIM_SLOT_0, true);
+}
+
+static model_list_t *active_model_list(void)
+{
+ return (current_source == MODEL_SOURCE_SD) ? &sd_models : &rom_models;
+}
+
+static void load_current_model(void)
+{
+ if (model) {
+ // Ensure previous frame rendering is done before freeing the model.
+ rspq_wait();
+ model64_free(model);
+ model = NULL;
+ }
+ anim_count = 0; // Clear list
+ tint_mesh = true;
+
+ model_list_t *list = active_model_list();
+ active_is_sd = (current_source == MODEL_SOURCE_SD);
+ if (!list || list->count <= 0)
+ return;
+
+ if (model_index < 0 || model_index >= list->count)
+ model_index = 0;
+
+ const model_entry_t *entry = &list->entries[model_index];
+ const char *path = entry->path;
+ model = model64_load(path);
+ if (!model)
+ return;
+
+ build_anim_list(path);
+ anim_index = 0;
+ play_current_anim();
+}
+
+static void setup_gl(void)
+{
+ float aspect = (float)display_get_width() / (float)display_get_height();
+ float near_plane = 1.0f;
+ float far_plane = 50.0f;
+
+ glMatrixMode(GL_PROJECTION);
+ glLoadIdentity();
+ glFrustum(-near_plane * aspect, near_plane * aspect, -near_plane,
+ near_plane, near_plane, far_plane);
+
+ glMatrixMode(GL_MODELVIEW);
+ glLoadIdentity();
+
+ setup_lighting();
+ glDisable(GL_TEXTURE_2D);
+ glEnable(GL_DEPTH_TEST);
+ glEnable(GL_CULL_FACE);
+}
+
+static void init(void)
+{
+ debug_init_emulog();
+ debug_init_usblog();
+
+ dfs_init(DFS_DEFAULT_LOCATION);
+ display_init(RESOLUTION_320x240, DEPTH_16_BPP, 2, GAMMA_NONE,
+ FILTERS_RESAMPLE);
+ rdpq_init();
+ gl_init();
+ joypad_init();
+ timer_init();
+
+ sd_available = debug_init_sdfs("sd:/", -1);
+
+ rdpq_font_t *font = rdpq_font_load_builtin(FONT_BUILTIN_DEBUG_MONO);
+ rdpq_text_register_font(font_id, font);
+
+ setup_gl();
+ scan_models("rom:/models", &rom_models);
+ if (sd_available)
+ scan_models("sd:/models", &sd_models);
+ load_current_model();
+}
+
+static void handle_input(joypad_buttons_t pressed)
+{
+ if (pressed.l) {
+ if (current_source != MODEL_SOURCE_ROM) {
+ current_source = MODEL_SOURCE_ROM;
+ model_index = 0;
+ load_current_model();
+ }
+ }
+ if (pressed.r) {
+ if (current_source != MODEL_SOURCE_SD) {
+ current_source = MODEL_SOURCE_SD;
+ model_index = 0;
+ load_current_model();
+ }
+ }
+
+ model_list_t *list = active_model_list();
+ if (pressed.d_left && list && list->count > 0) {
+ model_index = (model_index - 1 + list->count) % list->count;
+ load_current_model();
+ }
+ if (pressed.d_right && list && list->count > 0) {
+ model_index = (model_index + 1) % list->count;
+ load_current_model();
+ }
+ if (pressed.c_left && anim_count > 0) {
+ anim_index = (anim_index - 1 + anim_count) % anim_count;
+ play_current_anim();
+ }
+ if (pressed.c_right && anim_count > 0) {
+ anim_index = (anim_index + 1) % anim_count;
+ play_current_anim();
+ }
+
+ // Optional additonal render passes
+ if (pressed.c_up) {
+ use_shadowpass = !use_shadowpass;
+ }
+ if (pressed.c_down) {
+ use_outlinepass = !use_outlinepass;
+ }
+}
+
+static void update(uint32_t *last_ms, float *rotation)
+{
+ uint32_t now_ms = get_ticks_ms();
+ float dt = (now_ms - *last_ms) / 1000.0f;
+ *last_ms = now_ms;
+
+ *rotation += dt * 30.0f;
+ if (*rotation >= 360.0f)
+ *rotation -= 360.0f;
+
+ if (model) {
+ model64_update(model, dt);
+ }
+}
+
+static void draw_model(void)
+{
+ if (!model)
+ return;
+
+ if (use_shadowpass) {
+ glDisable(GL_LIGHTING);
+ glPushMatrix();
+ glTranslatef(0.0f, -0.7f, 0.0f);
+ glScalef(1.1f, 0.02f, 1.1f);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ glColor4f(0.0f, 0.0f, 0.0f, 0.35f);
+ model64_draw(model);
+ glPopMatrix();
+ glEnable(GL_LIGHTING);
+ }
+
+ if (use_outlinepass) {
+ glDisable(GL_LIGHTING);
+ glPushMatrix();
+ glScalef(1.03f, 1.03f, 1.03f);
+ glCullFace(GL_FRONT);
+ glColor4f(0.0f, 0.0f, 0.0f, 1.0f);
+ model64_draw(model);
+ glCullFace(GL_BACK);
+ glPopMatrix();
+ glEnable(GL_LIGHTING);
+ }
+
+ // Mesh color based on source location
+ if (tint_mesh) {
+ if (active_is_sd) {
+ glColor4f(0.30f, 0.454f, 0.6f, 1.0f);
+ } else {
+ glColor4f(0.2313f, 0.427f, 0.384f, 1.0f);
+ }
+ } else {
+ glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
+ }
+ model64_draw(model);
+}
+
+static void render(float rotation)
+{
+ model_list_t *list = active_model_list();
+
+ surface_t *disp = display_get();
+ surface_t *zbuf = display_get_zbuf();
+
+ rdpq_attach(disp, zbuf);
+ gl_context_begin();
+
+ glClearColor(0.243f, 0.25f, 0.33f, 1.0f); // BG color
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+ glMatrixMode(GL_MODELVIEW);
+ glLoadIdentity();
+ glTranslatef(0.0f, -0.5f, -3.0f);
+ update_light_positions();
+ glRotatef(rotation, 0.0f, 1.0f, 0.0f);
+
+ draw_model();
+
+ gl_context_end();
+
+ rdpq_set_mode_standard();
+ const char *source = active_is_sd ? "SD" : "ROM";
+ const char *anim = (anim_count > 0 && anim_names[anim_index])
+ ? anim_names[anim_index]
+ : "None";
+ const char *model_name =
+ (list && list->count > 0) ? list->entries[model_index].name : "None";
+
+ const char *no_models_msg = "";
+ if (rom_models.count + sd_models.count == 0) {
+ no_models_msg = sd_available
+ ? "\nNo models found in ROM or SD."
+ : "\nNo models found in ROM. SD not mounted.";
+ } else if (!list || list->count == 0) {
+ if (active_is_sd && !sd_available) {
+ no_models_msg = "\nSD not mounted.";
+ } else {
+ no_models_msg = (active_is_sd) ? "\nNo models found on SD."
+ : "\nNo models found in ROM.";
+ }
+ }
+
+ rdpq_text_printf(NULL, font_id, 8, 8,
+ "\nModel: %s\nAnim: %s\nSource: %s%s\nD-Left/Right: "
+ "model C-Left/Right: anim\nC-Up: shadow C-Down: "
+ "outline\nL: ROM R: SD%s",
+ model_name, anim, source,
+ (!sd_available && active_is_sd) ? " (SD not mounted)" : "",
+ no_models_msg);
+
+ rdpq_detach_show();
+}
+
+int main(void)
+{
+ init();
+
+ uint32_t last_ms = get_ticks_ms();
+ float rotation = 0.0f;
+
+ while (1) {
+ joypad_poll();
+ joypad_buttons_t pressed = joypad_get_buttons_pressed(JOYPAD_PORT_1);
+ handle_input(pressed);
+ update(&last_ms, &rotation);
+ render(rotation);
+ }
+}
diff --git a/examples/meshviewer/readme.md b/examples/meshviewer/readme.md
new file mode 100644
index 000000000..1ee2f0f48
--- /dev/null
+++ b/examples/meshviewer/readme.md
@@ -0,0 +1,37 @@
+# model64 Mesh Viewer
+
+A minimal model64 viewer example for libdragon.
+
+It demonstrates ROM/SD asset loading, model switching, and animation playback.
+
+**Controls**
+
+- L/R: switch model
+- C-Left/C-Right: switch animation
+- C-Up: toggle Shadow
+- C-Down: toggle Outline
+- Z: toggle ROM/SD source
+
+**SD usage**
+
+Copy `filesystem/models/*.model64` to `sd:/models/`.
+If SD is not mounted, assets load always from ROM (DFS).
+
+Tested on my SummerCart64.
+
+---
+
+### How to extend this code
+
+#### Export GLB Mesh without Texture
+
+Use GLB when you want a quick, untextured model.
+1) Export from Blender as `.glb`.
+2) Drop it in `assets/` and add it to `models[]` in the top of `meshviewer.c`.
+
+#### Export GLTF + BIN + PNG Texture
+
+Use this when you need textures.
+1) Export as glTF Separate: `.gltf` + `.bin` + `.png`.
+2) Keep the PNG next to the `.gltf` (same folder).
+3) Add the model to `models[]` in `meshviewer.c` and rebuild.
diff --git a/examples/mghello/.gitignore b/examples/mghello/.gitignore
new file mode 100644
index 000000000..87ef66815
--- /dev/null
+++ b/examples/mghello/.gitignore
@@ -0,0 +1 @@
+filesystem/
\ No newline at end of file
diff --git a/examples/mghello/Makefile b/examples/mghello/Makefile
new file mode 100644
index 000000000..caffdd65e
--- /dev/null
+++ b/examples/mghello/Makefile
@@ -0,0 +1,21 @@
+BUILD_DIR=build
+# This example uses magma, which is a preview API
+LIBDRAGON_PREVIEW = 2
+include $(N64_INST)/include/n64.mk
+
+src = mghello.c
+
+MKSPRITE_FLAGS ?=
+
+all: mghello.z64
+
+$(BUILD_DIR)/mghello.elf: $(src:%.c=$(BUILD_DIR)/%.o)
+
+mghello.z64: N64_ROM_TITLE="Magma Hello"
+
+clean:
+ rm -rf $(BUILD_DIR) mghello.z64
+
+-include $(wildcard $(BUILD_DIR)/*.d)
+
+.PHONY: all clean
diff --git a/examples/mghello/README.md b/examples/mghello/README.md
new file mode 100644
index 000000000..2a5570560
--- /dev/null
+++ b/examples/mghello/README.md
@@ -0,0 +1,8 @@
+# Magma Hello Triangle
+
+This is a minimal example of how to get something on the screen using magma, libdragon's builtin high performance 3D graphics API.
+In typical "Hello Triangle" fashion, it simply renders a single static triangle with vertex colors.
+
+Please note that this example only showcases a subset of magma's features due to its simplicity.
+It can be seen as an introduction to some of the API's basic concepts.
+If you'd like to see a more complete example of how to use magma for more realistic rendering tasks including best practices, check out the "mgdemo" example.
diff --git a/examples/mghello/mghello.c b/examples/mghello/mghello.c
new file mode 100644
index 000000000..64d90b32b
--- /dev/null
+++ b/examples/mghello/mghello.c
@@ -0,0 +1,136 @@
+#include
+
+typedef struct {
+ int16_t pos[3];
+} vertex;
+
+typedef struct {
+ mgfx_fog_t fog;
+ mgfx_lighting_t lighting;
+ mgfx_texturing_t texturing;
+ mgfx_matrices_t matrices;
+} uniforms;
+
+int main()
+{
+ const resolution_t resolution = RESOLUTION_320x240;
+
+ debug_init(DEBUG_FEATURE_LOG_EMU | DEBUG_FEATURE_LOG_USB);
+ display_init(resolution, DEPTH_16_BPP, 3, GAMMA_NONE, FILTERS_RESAMPLE_ANTIALIAS_DEDITHER);
+ rdpq_init();
+ mg_init();
+
+ // Get the builtin rendering pipeline.
+ // We need to configure the layout of our vertex data. Magma will then patch the pipeline according to this layout.
+ mg_vertex_attribute_t vertex_attributes[] = {
+ {
+ .input = MGFX_ATTRIBUTE_POSITION,
+ .offset = offsetof(vertex, pos)
+ },
+ };
+ mg_pipeline_t *pipeline = mg_pipeline_create(&(mg_pipeline_parms_t) {
+ .vertex_shader_ucode = mgfx_get_shader_ucode(0),
+ .vertex_layout.attribute_count = sizeof(vertex_attributes)/sizeof(vertex_attributes[0]),
+ .vertex_layout.attributes = vertex_attributes,
+ .vertex_layout.stride = sizeof(vertex)
+ });
+
+ // Shader uniforms are not initialized to defaults automatically, so we need to create
+ // a uniform buffer that sets everything to sane values.
+ uniforms *uniform_data = malloc_uncached(sizeof(uniforms));
+
+ // This disables fog completely, because both start and end are initialized to 0.
+ mgfx_get_fog(&uniform_data->fog, &(mgfx_fog_parms_t) {});
+
+ // Lighting is never explicitly turned off, but it can be set to "pass through"
+ // by configuring 0 lights and fully white ambient light.
+ mgfx_get_lighting(&uniform_data->lighting, &(mgfx_lighting_parms_t) {
+ .light_count = 0,
+ .ambient_color = color_from_packed32(0xFFFFFFFF)
+ });
+
+ // Setup simple orthographic projection
+ fm_mat4_t projection, model_view;
+ mg_mat4_ortho(&projection, -100, 100, -100, 100, -100, 100);
+ fm_mat4_identity(&model_view);
+ mgfx_get_matrices(&uniform_data->matrices, &(mgfx_matrices_parms_t) {
+ .model_view_projection = projection.m[0],
+ .model_view = model_view.m[0],
+ .normal = model_view.m[0],
+ });
+
+ const mg_uniform_t *fog_uniform = mg_pipeline_get_uniform(pipeline, MGFX_BINDING_FOG);
+ const mg_uniform_t *lighting_uniform = mg_pipeline_get_uniform(pipeline, MGFX_BINDING_LIGHTING);
+ const mg_uniform_t *texturing_uniform = mg_pipeline_get_uniform(pipeline, MGFX_BINDING_TEXTURING);
+ const mg_uniform_t *matrices_uniform = mg_pipeline_get_uniform(pipeline, MGFX_BINDING_MATRICES);
+
+ // Create and fill a vertex buffer.
+ vertex *vertices = malloc_uncached(sizeof(vertex) * 3);
+ vertices[0] = (vertex){ .pos = { 0, 50, 0 }};
+ vertices[1] = (vertex){ .pos = {-50, -50, 0 }};
+ vertices[2] = (vertex){ .pos = { 50, -50, 0 }};
+
+ // Everything we need is initialised. Start the main rendering loop!
+ while (true)
+ {
+ // This is just the regular display + rdpq setup.
+ surface_t *disp = display_get();
+
+ rdpq_attach_clear(disp, NULL);
+
+ rdpq_mode_begin();
+ rdpq_set_mode_standard();
+ rdpq_mode_dithering(DITHER_SQUARE_SQUARE);
+ rdpq_mode_antialias(AA_STANDARD);
+ rdpq_mode_combiner(RDPQ_COMBINER_FLAT);
+ rdpq_mode_end();
+
+ rdpq_set_prim_color(color_from_packed32(0xFFFFFFFF));
+
+ // Set the vertex pipeline
+ mg_pipeline_bind(pipeline);
+
+ // Set the viewport to the full screen
+ // Note: mg_set_viewport_fullscreen(resolution) can also be used as a shorthand.
+ mg_set_viewport(&(mg_viewport_t) {
+ .x = 0,
+ .y = 0,
+ .width = resolution.width,
+ .height = resolution.height,
+ .minDepth = 0.0f,
+ .maxDepth = 1.0f,
+ });
+
+ // Set the culling mode.
+ mg_set_culling(&(mg_culling_parms_t) {
+ .cull_mode = MG_CULL_MODE_NONE
+ });
+
+ // Configure the type of triangles that should be emitted to the RDP.
+ mg_set_geometry_flags(0);
+
+ // Load uniforms. This must be done every frame to guarantee that the uniforms have the desired values.
+ mg_uniform_load(fog_uniform, &uniform_data->fog);
+ mg_uniform_load(lighting_uniform, &uniform_data->lighting);
+ mg_uniform_load(texturing_uniform, &uniform_data->texturing);
+ mg_uniform_load(matrices_uniform, &uniform_data->matrices);
+
+ // Bind the vertex buffer that was created above. All subsequent drawing
+ // commands will now read from this buffer.
+ mg_bind_vertex_buffer(vertices);
+
+ // All drawing commands (including mg_draw and mg_draw_indexed) must be put
+ // between mg_draw_begin and mg_draw_end, which delimit a drawing "batch".
+ // This is to guarantee proper synchronisation with render modes.
+ // Render modes must not be changed during a drawing batch.
+ mg_draw_begin();
+ // Load all vertices from the buffer into the internal cache
+ mg_load_vertices(0, 0, 3);
+ // Draw a triangle using those vertices
+ mg_draw_triangle(0, 1, 2);
+ mg_draw_end();
+
+ // End the frame as usual.
+ rdpq_detach_show();
+ }
+}
diff --git a/examples/micro-ui/.gitignore b/examples/micro-ui/.gitignore
new file mode 100644
index 000000000..b2552045c
--- /dev/null
+++ b/examples/micro-ui/.gitignore
@@ -0,0 +1,3 @@
+filesystem/
+build/
+*.z64
diff --git a/examples/micro-ui/Makefile b/examples/micro-ui/Makefile
new file mode 100644
index 000000000..cf3827c5d
--- /dev/null
+++ b/examples/micro-ui/Makefile
@@ -0,0 +1,39 @@
+BUILD_DIR = build
+# This example uses OpenGL, which is a preview API
+LIBDRAGON_PREVIEW = 2
+include $(N64_INST)/include/n64.mk
+
+assets_ttf = $(wildcard assets/*.ttf)
+assets_png = $(wildcard assets/*.png)
+
+assets_conv = $(addprefix filesystem/,$(notdir $(assets_ttf:%.ttf=%.font64))) \
+ $(addprefix filesystem/,$(notdir $(assets_png:%.png=%.sprite)))
+
+OBJS = $(BUILD_DIR)/micro-ui.o \
+ $(BUILD_DIR)/lib/microui.o \
+ $(BUILD_DIR)/lib/microuiN64.o
+
+all: micro-ui.z64
+
+filesystem/%.font64: assets/%.ttf
+ @mkdir -p $(dir $@)
+ @echo " [FONT] $@"
+ $(N64_MKFONT) $(MKFONT_FLAGS) -s 9 -o filesystem "$<"
+
+filesystem/%.sprite: assets/%.png
+ @mkdir -p $(dir $@)
+ @echo " [SPRITE] $@"
+ $(N64_MKSPRITE) $(MKSPRITE_FLAGS) -o filesystem "$<"
+
+$(BUILD_DIR)/micro-ui.dfs: $(assets_conv)
+$(BUILD_DIR)/micro-ui.elf: $(OBJS)
+
+micro-ui.z64: N64_ROM_TITLE = "MicroUI N64"
+micro-ui.z64: $(BUILD_DIR)/micro-ui.dfs
+
+clean:
+ rm -rf $(BUILD_DIR) *.z64 filesystem
+
+-include $(wildcard $(BUILD_DIR)/*.d)
+
+.PHONY: all clean
\ No newline at end of file
diff --git a/examples/micro-ui/Readme.md b/examples/micro-ui/Readme.md
new file mode 100644
index 000000000..b895006c8
--- /dev/null
+++ b/examples/micro-ui/Readme.md
@@ -0,0 +1,15 @@
+# MicroUI N64
+
+This example contains the immediate-mode UI (micro-UI) as an N64 port.
+It's mainly intended for quick and easy debugging and editor UIs, similar to imgui.
+The main focus lies on being able to create UI elements on the fly from everywhere in your code,
+with the ability to change variables by passing them as pointers.
+For more complex interactive game UIs, a different library should be used.
+
+Interaction with the UI is handled with a mouse-cursor.
+This cursor can either be a real N64-mouse, or simulated mouse via the control-stick.
+
+The library itself is contained in `lib/microui.c`, whereas the N64 specific wrapper is in `lib/microuiN64.c`.
+
+## Links
+Original repo: https://github.com/rxi/microui
\ No newline at end of file
diff --git a/examples/micro-ui/assets/VCR_OSD_MONO.ttf b/examples/micro-ui/assets/VCR_OSD_MONO.ttf
new file mode 100644
index 000000000..dcca687a4
Binary files /dev/null and b/examples/micro-ui/assets/VCR_OSD_MONO.ttf differ
diff --git a/examples/micro-ui/assets/mui.png b/examples/micro-ui/assets/mui.png
new file mode 100644
index 000000000..c3a5dff2c
Binary files /dev/null and b/examples/micro-ui/assets/mui.png differ
diff --git a/examples/micro-ui/assets/test_10m.png b/examples/micro-ui/assets/test_10m.png
new file mode 100644
index 000000000..718e432dc
Binary files /dev/null and b/examples/micro-ui/assets/test_10m.png differ
diff --git a/examples/micro-ui/assets/test_1m.png b/examples/micro-ui/assets/test_1m.png
new file mode 100644
index 000000000..03ac50c89
Binary files /dev/null and b/examples/micro-ui/assets/test_1m.png differ
diff --git a/examples/micro-ui/assets/test_2m.png b/examples/micro-ui/assets/test_2m.png
new file mode 100644
index 000000000..a669e796b
Binary files /dev/null and b/examples/micro-ui/assets/test_2m.png differ
diff --git a/examples/micro-ui/assets/test_5m.png b/examples/micro-ui/assets/test_5m.png
new file mode 100644
index 000000000..88cbf3041
Binary files /dev/null and b/examples/micro-ui/assets/test_5m.png differ
diff --git a/examples/micro-ui/demo-helper.h b/examples/micro-ui/demo-helper.h
new file mode 100644
index 000000000..d2d3a7586
--- /dev/null
+++ b/examples/micro-ui/demo-helper.h
@@ -0,0 +1,75 @@
+#ifndef DEMO_HELPER_H
+#define DEMO_HELPER_H
+
+void draw_cube()
+{
+ glEnable(GL_COLOR_MATERIAL);
+ glEnable(GL_LIGHTING);
+ glBegin(GL_QUADS);
+
+ glColor3f(1.0f, 1.0f, 1.0f);
+ glVertex3f(1.0f, 1.0f, -1.0f);
+ glVertex3f(-1.0f, 1.0f, -1.0f);
+ glVertex3f(-1.0f, 1.0f, 1.0f);
+ glVertex3f(1.0f, 1.0f, 1.0f);
+
+ glColor3f(1.0f, 0.5f, 0.0f);
+ glVertex3f(1.0f, -1.0f, 1.0f);
+ glVertex3f(-1.0f, -1.0f, 1.0f);
+ glVertex3f(-1.0f, -1.0f, -1.0f);
+ glVertex3f(1.0f, -1.0f, -1.0f);
+
+ glColor3f(1.0f, 0.0f, 0.0f);
+ glVertex3f(1.0f, 1.0f, 1.0f);
+ glVertex3f(-1.0f, 1.0f, 1.0f);
+ glVertex3f(-1.0f, -1.0f, 1.0f);
+ glVertex3f(1.0f, -1.0f, 1.0f);
+
+ glColor3f(1.0f, 1.0f, 0.0f);
+ glVertex3f(1.0f, -1.0f, -1.0f);
+ glVertex3f(-1.0f, -1.0f, -1.0f);
+ glVertex3f(-1.0f, 1.0f, -1.0f);
+ glVertex3f(1.0f, 1.0f, -1.0f);
+
+ glColor3f(0.0f, 0.0f, 1.0f);
+ glVertex3f(-1.0f, 1.0f, 1.0f);
+ glVertex3f(-1.0f, 1.0f, -1.0f);
+ glVertex3f(-1.0f, -1.0f, -1.0f);
+ glVertex3f(-1.0f, -1.0f, 1.0f);
+
+ glColor3f(1.0f, 0.0f, 1.0f);
+ glVertex3f(1.0f, 1.0f, -1.0f);
+ glVertex3f(1.0f, 1.0f, 1.0f);
+ glVertex3f(1.0f, -1.0f, 1.0f);
+ glVertex3f(1.0f, -1.0f, -1.0f);
+ glEnd();
+}
+
+int create_cube_dpl()
+{
+ int dlCube = glGenLists(1);
+ glNewList(dlCube, GL_COMPILE);
+
+ draw_cube();
+
+ glEndList();
+ return dlCube;
+}
+
+void rainbow(float ratio, float *r, float *g, float *b)
+{
+ float lum = 0.68f;
+ float normalized = ratio * 6.0f;
+ float x = fmodf(normalized, 1.0f) * lum;
+ switch ((int)normalized)
+ {
+ case 0: *r = lum; *g = 0; *b = 0; *g += x; break;
+ case 1: *r = lum; *g = lum; *b = 0; *r -= x; break;
+ case 2: *r = 0; *g = lum; *b = 0; *b += x; break;
+ case 3: *r = 0; *g = lum; *b = lum; *g -= x; break;
+ case 4: *r = 0; *g = 0; *b = lum; *r += x; break;
+ case 5: *r = lum; *g = 0; *b = lum; *b -= x; break;
+ }
+}
+
+#endif
\ No newline at end of file
diff --git a/examples/micro-ui/lib/microui.c b/examples/micro-ui/lib/microui.c
new file mode 100644
index 000000000..f8c03873f
--- /dev/null
+++ b/examples/micro-ui/lib/microui.c
@@ -0,0 +1,1254 @@
+/*
+** Copyright (c) 2024 rxi
+**
+** 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.
+*/
+
+#include
+#include
+#include
+#include "microui.h"
+
+#define unused(x) ((void) (x))
+
+#define expect(x) do { \
+ if (!(x)) { \
+ fprintf(stderr, "Fatal error: %s:%d: assertion '%s' failed\n", \
+ __FILE__, __LINE__, #x); \
+ abort(); \
+ } \
+ } while (0)
+
+#define push(stk, val) do { \
+ expect((stk).idx < (int) (sizeof((stk).items) / sizeof(*(stk).items))); \
+ (stk).items[(stk).idx] = (val); \
+ (stk).idx++; /* incremented after incase `val` uses this value */ \
+ } while (0)
+
+#define pop(stk) do { \
+ expect((stk).idx > 0); \
+ (stk).idx--; \
+ } while (0)
+
+
+static mu_Rect unclipped_rect = { 0, 0, 0x1000000, 0x1000000 };
+
+static mu_Style default_style = {
+ /* font | size | padding | spacing | indent */
+ NULL, { 68, 10 }, 5, 4, 24,
+ /* title_height | scrollbar_size | thumb_size */
+ 24, 12, 8,
+ {
+ { 230, 230, 230, 255 }, /* MU_COLOR_TEXT */
+ { 25, 25, 25, 255 }, /* MU_COLOR_BORDER */
+ { 50, 50, 50, 255 }, /* MU_COLOR_WINDOWBG */
+ { 25, 25, 25, 255 }, /* MU_COLOR_TITLEBG */
+ { 240, 240, 240, 255 }, /* MU_COLOR_TITLETEXT */
+ { 0, 0, 0, 0 }, /* MU_COLOR_PANELBG */
+ { 75, 75, 75, 255 }, /* MU_COLOR_BUTTON */
+ { 95, 95, 95, 255 }, /* MU_COLOR_BUTTONHOVER */
+ { 115, 115, 115, 255 }, /* MU_COLOR_BUTTONFOCUS */
+ { 30, 30, 30, 255 }, /* MU_COLOR_BASE */
+ { 35, 35, 35, 255 }, /* MU_COLOR_BASEHOVER */
+ { 40, 40, 40, 255 }, /* MU_COLOR_BASEFOCUS */
+ { 43, 43, 43, 255 }, /* MU_COLOR_SCROLLBASE */
+ { 30, 30, 30, 255 } /* MU_COLOR_SCROLLTHUMB */
+ }
+};
+
+
+mu_Vec2 mu_vec2(int x, int y) {
+ mu_Vec2 res;
+ res.x = x; res.y = y;
+ return res;
+}
+
+
+mu_Rect mu_rect(int x, int y, int w, int h) {
+ mu_Rect res;
+ res.x = x; res.y = y; res.w = w; res.h = h;
+ return res;
+}
+
+
+mu_Color mu_color(int r, int g, int b, int a) {
+ mu_Color res;
+ res.r = r; res.g = g; res.b = b; res.a = a;
+ return res;
+}
+
+
+static mu_Rect expand_rect(mu_Rect rect, int n) {
+ return mu_rect(rect.x - n, rect.y - n, rect.w + n * 2, rect.h + n * 2);
+}
+
+
+static mu_Rect intersect_rects(mu_Rect r1, mu_Rect r2) {
+ int x1 = mu_max(r1.x, r2.x);
+ int y1 = mu_max(r1.y, r2.y);
+ int x2 = mu_min(r1.x + r1.w, r2.x + r2.w);
+ int y2 = mu_min(r1.y + r1.h, r2.y + r2.h);
+ if (x2 < x1) { x2 = x1; }
+ if (y2 < y1) { y2 = y1; }
+ return mu_rect(x1, y1, x2 - x1, y2 - y1);
+}
+
+
+static int rect_overlaps_vec2(mu_Rect r, mu_Vec2 p) {
+ return p.x >= r.x && p.x < r.x + r.w && p.y >= r.y && p.y < r.y + r.h;
+}
+
+
+static void draw_frame(mu_Context *ctx, mu_Rect rect, int colorid) {
+ mu_draw_rect(ctx, rect, ctx->style->colors[colorid]);
+ if (colorid == MU_COLOR_SCROLLBASE ||
+ colorid == MU_COLOR_SCROLLTHUMB ||
+ colorid == MU_COLOR_TITLEBG) { return; }
+ /* draw border */
+ if (ctx->style->colors[MU_COLOR_BORDER].a) {
+ mu_draw_box(ctx, expand_rect(rect, 1), ctx->style->colors[MU_COLOR_BORDER]);
+ }
+}
+
+
+void mu_init(mu_Context *ctx) {
+ memset(ctx, 0, sizeof(*ctx));
+ ctx->draw_frame = draw_frame;
+ ctx->_style = default_style;
+ ctx->style = &ctx->_style;
+}
+
+
+void mu_begin(mu_Context *ctx) {
+ expect(ctx->text_width && ctx->text_height);
+ ctx->command_list.idx = 0;
+ ctx->root_list.idx = 0;
+ ctx->scroll_target = NULL;
+ ctx->hover_root = ctx->next_hover_root;
+ ctx->next_hover_root = NULL;
+ ctx->mouse_delta.x = ctx->mouse_pos.x - ctx->last_mouse_pos.x;
+ ctx->mouse_delta.y = ctx->mouse_pos.y - ctx->last_mouse_pos.y;
+ ctx->frame++;
+}
+
+
+static int compare_zindex(const void *a, const void *b) {
+ return (*(mu_Container**) a)->zindex - (*(mu_Container**) b)->zindex;
+}
+
+
+void mu_end(mu_Context *ctx) {
+ int i, n;
+ /* check stacks */
+ expect(ctx->container_stack.idx == 0);
+ expect(ctx->clip_stack.idx == 0);
+ expect(ctx->id_stack.idx == 0);
+ expect(ctx->layout_stack.idx == 0);
+
+ /* handle scroll input */
+ if (ctx->scroll_target) {
+ ctx->scroll_target->scroll.x += ctx->scroll_delta.x;
+ ctx->scroll_target->scroll.y += ctx->scroll_delta.y;
+ }
+
+ /* unset focus if focus id was not touched this frame */
+ if (!ctx->updated_focus) { ctx->focus = 0; }
+ ctx->updated_focus = 0;
+
+ /* bring hover root to front if mouse was pressed */
+ if (ctx->mouse_pressed && ctx->next_hover_root &&
+ ctx->next_hover_root->zindex < ctx->last_zindex &&
+ ctx->next_hover_root->zindex >= 0
+ ) {
+ mu_bring_to_front(ctx, ctx->next_hover_root);
+ }
+
+ /* reset input state */
+ ctx->key_pressed = 0;
+ ctx->input_text[0] = '\0';
+ ctx->mouse_pressed = 0;
+ ctx->scroll_delta = mu_vec2(0, 0);
+ ctx->last_mouse_pos = ctx->mouse_pos;
+
+ /* sort root containers by zindex */
+ n = ctx->root_list.idx;
+ qsort(ctx->root_list.items, n, sizeof(mu_Container*), compare_zindex);
+
+ /* set root container jump commands */
+ for (i = 0; i < n; i++) {
+ mu_Container *cnt = ctx->root_list.items[i];
+ /* if this is the first container then make the first command jump to it.
+ ** otherwise set the previous container's tail to jump to this one */
+ if (i == 0) {
+ mu_Command *cmd = (mu_Command*) ctx->command_list.items;
+ cmd->jump.dst = (char*) cnt->head + sizeof(mu_JumpCommand);
+ } else {
+ mu_Container *prev = ctx->root_list.items[i - 1];
+ prev->tail->jump.dst = (char*) cnt->head + sizeof(mu_JumpCommand);
+ }
+ /* make the last container's tail jump to the end of command list */
+ if (i == n - 1) {
+ cnt->tail->jump.dst = ctx->command_list.items + ctx->command_list.idx;
+ }
+ }
+}
+
+
+void mu_set_focus(mu_Context *ctx, mu_Id id) {
+ ctx->focus = id;
+ ctx->updated_focus = 1;
+}
+
+
+/* 32bit fnv-1a hash */
+#define HASH_INITIAL 2166136261
+
+static void hash(mu_Id *hash, const void *data, int size) {
+ const unsigned char *p = data;
+ while (size--) {
+ *hash = (*hash ^ *p++) * 16777619;
+ }
+}
+
+
+mu_Id mu_get_id(mu_Context *ctx, const void *data, int size) {
+ int idx = ctx->id_stack.idx;
+ mu_Id res = (idx > 0) ? ctx->id_stack.items[idx - 1] : HASH_INITIAL;
+ hash(&res, data, size);
+ ctx->last_id = res;
+ return res;
+}
+
+
+void mu_push_id(mu_Context *ctx, const void *data, int size) {
+ push(ctx->id_stack, mu_get_id(ctx, data, size));
+}
+
+
+void mu_pop_id(mu_Context *ctx) {
+ pop(ctx->id_stack);
+}
+
+
+void mu_push_clip_rect(mu_Context *ctx, mu_Rect rect) {
+ mu_Rect last = mu_get_clip_rect(ctx);
+ push(ctx->clip_stack, intersect_rects(rect, last));
+}
+
+
+void mu_pop_clip_rect(mu_Context *ctx) {
+ pop(ctx->clip_stack);
+}
+
+
+mu_Rect mu_get_clip_rect(mu_Context *ctx) {
+ expect(ctx->clip_stack.idx > 0);
+ return ctx->clip_stack.items[ctx->clip_stack.idx - 1];
+}
+
+
+int mu_check_clip(mu_Context *ctx, mu_Rect r) {
+ mu_Rect cr = mu_get_clip_rect(ctx);
+ if (r.x > cr.x + cr.w || r.x + r.w < cr.x ||
+ r.y > cr.y + cr.h || r.y + r.h < cr.y ) { return MU_CLIP_ALL; }
+ if (r.x >= cr.x && r.x + r.w <= cr.x + cr.w &&
+ r.y >= cr.y && r.y + r.h <= cr.y + cr.h ) { return 0; }
+ return MU_CLIP_PART;
+}
+
+
+static void push_layout(mu_Context *ctx, mu_Rect body, mu_Vec2 scroll) {
+ mu_Layout layout;
+ int width = 0;
+ memset(&layout, 0, sizeof(layout));
+ layout.body = mu_rect(body.x - scroll.x, body.y - scroll.y, body.w, body.h);
+ layout.max = mu_vec2(-0x1000000, -0x1000000);
+ push(ctx->layout_stack, layout);
+ mu_layout_row(ctx, 1, &width, 0);
+}
+
+
+static mu_Layout* get_layout(mu_Context *ctx) {
+ return &ctx->layout_stack.items[ctx->layout_stack.idx - 1];
+}
+
+
+static void pop_container(mu_Context *ctx) {
+ mu_Container *cnt = mu_get_current_container(ctx);
+ mu_Layout *layout = get_layout(ctx);
+ cnt->content_size.x = layout->max.x - layout->body.x;
+ cnt->content_size.y = layout->max.y - layout->body.y;
+ /* pop container, layout and id */
+ pop(ctx->container_stack);
+ pop(ctx->layout_stack);
+ mu_pop_id(ctx);
+}
+
+
+mu_Container* mu_get_current_container(mu_Context *ctx) {
+ expect(ctx->container_stack.idx > 0);
+ return ctx->container_stack.items[ ctx->container_stack.idx - 1 ];
+}
+
+
+static mu_Container* get_container(mu_Context *ctx, mu_Id id, int opt) {
+ mu_Container *cnt;
+ /* try to get existing container from pool */
+ int idx = mu_pool_get(ctx, ctx->container_pool, MU_CONTAINERPOOL_SIZE, id);
+ if (idx >= 0) {
+ if (ctx->containers[idx].open || ~opt & MU_OPT_CLOSED) {
+ mu_pool_update(ctx, ctx->container_pool, idx);
+ }
+ return &ctx->containers[idx];
+ }
+ if (opt & MU_OPT_CLOSED) { return NULL; }
+ /* container not found in pool: init new container */
+ idx = mu_pool_init(ctx, ctx->container_pool, MU_CONTAINERPOOL_SIZE, id);
+ cnt = &ctx->containers[idx];
+ memset(cnt, 0, sizeof(*cnt));
+ cnt->open = 1;
+ mu_bring_to_front(ctx, cnt);
+ return cnt;
+}
+
+
+mu_Container* mu_get_container(mu_Context *ctx, const char *name) {
+ mu_Id id = mu_get_id(ctx, name, strlen(name));
+ return get_container(ctx, id, 0);
+}
+
+
+void mu_bring_to_front(mu_Context *ctx, mu_Container *cnt) {
+ cnt->zindex = ++ctx->last_zindex;
+}
+
+
+/*============================================================================
+** pool
+**============================================================================*/
+
+int mu_pool_init(mu_Context *ctx, mu_PoolItem *items, int len, mu_Id id) {
+ int i, n = -1, f = ctx->frame;
+ for (i = 0; i < len; i++) {
+ if (items[i].last_update < f) {
+ f = items[i].last_update;
+ n = i;
+ }
+ }
+ expect(n > -1);
+ items[n].id = id;
+ mu_pool_update(ctx, items, n);
+ return n;
+}
+
+
+int mu_pool_get(mu_Context *ctx, mu_PoolItem *items, int len, mu_Id id) {
+ int i;
+ unused(ctx);
+ for (i = 0; i < len; i++) {
+ if (items[i].id == id) { return i; }
+ }
+ return -1;
+}
+
+
+void mu_pool_update(mu_Context *ctx, mu_PoolItem *items, int idx) {
+ items[idx].last_update = ctx->frame;
+}
+
+
+/*============================================================================
+** input handlers
+**============================================================================*/
+
+void mu_input_mousemove(mu_Context *ctx, int x, int y) {
+ ctx->mouse_pos = mu_vec2(x, y);
+}
+
+
+void mu_input_mousedown(mu_Context *ctx, int x, int y, int btn) {
+ mu_input_mousemove(ctx, x, y);
+ ctx->mouse_down |= btn;
+ ctx->mouse_pressed |= btn;
+}
+
+
+void mu_input_mouseup(mu_Context *ctx, int x, int y, int btn) {
+ mu_input_mousemove(ctx, x, y);
+ ctx->mouse_down &= ~btn;
+}
+
+
+void mu_input_scroll(mu_Context *ctx, int x, int y) {
+ ctx->scroll_delta.x += x;
+ ctx->scroll_delta.y += y;
+}
+
+
+void mu_input_keydown(mu_Context *ctx, int key) {
+ ctx->key_pressed |= key;
+ ctx->key_down |= key;
+}
+
+
+void mu_input_keyup(mu_Context *ctx, int key) {
+ ctx->key_down &= ~key;
+}
+
+
+void mu_input_text(mu_Context *ctx, const char *text) {
+ int len = strlen(ctx->input_text);
+ int size = strlen(text) + 1;
+ expect(len + size <= (int) sizeof(ctx->input_text));
+ memcpy(ctx->input_text + len, text, size);
+}
+
+
+/*============================================================================
+** commandlist
+**============================================================================*/
+
+mu_Command* mu_push_command(mu_Context *ctx, int type, int size) {
+ mu_Command *cmd = (mu_Command*) (ctx->command_list.items + ctx->command_list.idx);
+#ifdef MU_N64_MODIFICATIONS
+ /* see https://github.com/rxi/microui/pull/76 for more details */
+ const int alignment = sizeof(void*) - 1;
+ size = (size + alignment) & ~alignment;
+#endif
+ expect(ctx->command_list.idx + size < MU_COMMANDLIST_SIZE);
+ cmd->base.type = type;
+ cmd->base.size = size;
+ ctx->command_list.idx += size;
+ return cmd;
+}
+
+
+int mu_next_command(mu_Context *ctx, mu_Command **cmd) {
+ if (*cmd) {
+ *cmd = (mu_Command*) (((char*) *cmd) + (*cmd)->base.size);
+ } else {
+ *cmd = (mu_Command*) ctx->command_list.items;
+ }
+ while ((char*) *cmd != ctx->command_list.items + ctx->command_list.idx) {
+ if ((*cmd)->type != MU_COMMAND_JUMP) { return 1; }
+ *cmd = (*cmd)->jump.dst;
+ }
+ return 0;
+}
+
+
+static mu_Command* push_jump(mu_Context *ctx, mu_Command *dst) {
+ mu_Command *cmd;
+ cmd = mu_push_command(ctx, MU_COMMAND_JUMP, sizeof(mu_JumpCommand));
+ cmd->jump.dst = dst;
+ return cmd;
+}
+
+
+void mu_set_clip(mu_Context *ctx, mu_Rect rect) {
+ mu_Command *cmd;
+ cmd = mu_push_command(ctx, MU_COMMAND_CLIP, sizeof(mu_ClipCommand));
+ cmd->clip.rect = rect;
+}
+
+
+void mu_draw_rect(mu_Context *ctx, mu_Rect rect, mu_Color color) {
+ mu_Command *cmd;
+ rect = intersect_rects(rect, mu_get_clip_rect(ctx));
+ if (rect.w > 0 && rect.h > 0) {
+ cmd = mu_push_command(ctx, MU_COMMAND_RECT, sizeof(mu_RectCommand));
+ cmd->rect.rect = rect;
+ cmd->rect.color = color;
+ }
+}
+
+
+void mu_draw_box(mu_Context *ctx, mu_Rect rect, mu_Color color) {
+ mu_draw_rect(ctx, mu_rect(rect.x + 1, rect.y, rect.w - 2, 1), color);
+ mu_draw_rect(ctx, mu_rect(rect.x + 1, rect.y + rect.h - 1, rect.w - 2, 1), color);
+ mu_draw_rect(ctx, mu_rect(rect.x, rect.y, 1, rect.h), color);
+ mu_draw_rect(ctx, mu_rect(rect.x + rect.w - 1, rect.y, 1, rect.h), color);
+}
+
+
+void mu_draw_text(mu_Context *ctx, mu_Font font, const char *str, int len,
+ mu_Vec2 pos, mu_Color color)
+{
+ mu_Command *cmd;
+ mu_Rect rect = mu_rect(
+ pos.x, pos.y, ctx->text_width(font, str, len), ctx->text_height(font));
+ int clipped = mu_check_clip(ctx, rect);
+ if (clipped == MU_CLIP_ALL ) { return; }
+ if (clipped == MU_CLIP_PART) { mu_set_clip(ctx, mu_get_clip_rect(ctx)); }
+ /* add command */
+ if (len < 0) { len = strlen(str); }
+ cmd = mu_push_command(ctx, MU_COMMAND_TEXT, sizeof(mu_TextCommand) + len);
+ memcpy(cmd->text.str, str, len);
+ cmd->text.str[len] = '\0';
+ cmd->text.pos = pos;
+ cmd->text.color = color;
+ cmd->text.font = font;
+ /* reset clipping if it was set */
+ if (clipped) { mu_set_clip(ctx, unclipped_rect); }
+}
+
+
+void mu_draw_icon(mu_Context *ctx, int id, mu_Rect rect, mu_Color color) {
+ mu_Command *cmd;
+ /* do clip command if the rect isn't fully contained within the cliprect */
+ int clipped = mu_check_clip(ctx, rect);
+ if (clipped == MU_CLIP_ALL ) { return; }
+ if (clipped == MU_CLIP_PART) { mu_set_clip(ctx, mu_get_clip_rect(ctx)); }
+ /* do icon command */
+ cmd = mu_push_command(ctx, MU_COMMAND_ICON, sizeof(mu_IconCommand));
+ cmd->icon.id = id;
+ cmd->icon.rect = rect;
+ cmd->icon.color = color;
+ /* reset clipping if it was set */
+ if (clipped) { mu_set_clip(ctx, unclipped_rect); }
+}
+
+#ifdef MU_N64_MODIFICATIONS
+
+void mu_draw_surface(mu_Context *ctx, const void* surf, mu_Rect rect) {
+ mu_Command *cmd;
+ /* do clip command if the rect isn't fully contained within the cliprect */
+ int clipped = mu_check_clip(ctx, rect);
+ if (clipped == MU_CLIP_ALL ) { return; }
+ if (clipped == MU_CLIP_PART) { mu_set_clip(ctx, mu_get_clip_rect(ctx)); }
+ /* do surface command */
+ cmd = mu_push_command(ctx, MU_COMMAND_SURFACE, sizeof(mu_SurfaceCommand));
+ cmd->surface.surface = surf;
+ cmd->surface.rect = rect;
+ /* reset clipping if it was set */
+ if (clipped) { mu_set_clip(ctx, unclipped_rect); }
+}
+
+
+void mu_draw_sprite(mu_Context *ctx, const void* sprite, mu_Rect rect) {
+ mu_Command *cmd;
+ /* do clip command if the rect isn't fully contained within the cliprect */
+ int clipped = mu_check_clip(ctx, rect);
+ if (clipped == MU_CLIP_ALL ) { return; }
+ if (clipped == MU_CLIP_PART) { mu_set_clip(ctx, mu_get_clip_rect(ctx)); }
+ /* do sprite command */
+ cmd = mu_push_command(ctx, MU_COMMAND_SPRITE, sizeof(mu_SpriteCommand));
+ cmd->sprite.sprite = sprite;
+ cmd->sprite.rect = rect;
+ /* reset clipping if it was set */
+ if (clipped) { mu_set_clip(ctx, unclipped_rect); }
+}
+
+#endif
+
+/*============================================================================
+** layout
+**============================================================================*/
+
+enum { RELATIVE = 1, ABSOLUTE = 2 };
+
+
+void mu_layout_begin_column(mu_Context *ctx) {
+ push_layout(ctx, mu_layout_next(ctx), mu_vec2(0, 0));
+}
+
+
+void mu_layout_end_column(mu_Context *ctx) {
+ mu_Layout *a, *b;
+ b = get_layout(ctx);
+ pop(ctx->layout_stack);
+ /* inherit position/next_row/max from child layout if they are greater */
+ a = get_layout(ctx);
+ a->position.x = mu_max(a->position.x, b->position.x + b->body.x - a->body.x);
+ a->next_row = mu_max(a->next_row, b->next_row + b->body.y - a->body.y);
+ a->max.x = mu_max(a->max.x, b->max.x);
+ a->max.y = mu_max(a->max.y, b->max.y);
+}
+
+
+void mu_layout_row(mu_Context *ctx, int items, const int *widths, int height) {
+ mu_Layout *layout = get_layout(ctx);
+ if (widths) {
+ expect(items <= MU_MAX_WIDTHS);
+ memcpy(layout->widths, widths, items * sizeof(widths[0]));
+ }
+ layout->items = items;
+ layout->position = mu_vec2(layout->indent, layout->next_row);
+ layout->size.y = height;
+ layout->item_index = 0;
+}
+
+
+void mu_layout_width(mu_Context *ctx, int width) {
+ get_layout(ctx)->size.x = width;
+}
+
+
+void mu_layout_height(mu_Context *ctx, int height) {
+ get_layout(ctx)->size.y = height;
+}
+
+
+void mu_layout_set_next(mu_Context *ctx, mu_Rect r, int relative) {
+ mu_Layout *layout = get_layout(ctx);
+ layout->next = r;
+ layout->next_type = relative ? RELATIVE : ABSOLUTE;
+}
+
+
+mu_Rect mu_layout_next(mu_Context *ctx) {
+ mu_Layout *layout = get_layout(ctx);
+ mu_Style *style = ctx->style;
+ mu_Rect res;
+
+ if (layout->next_type) {
+ /* handle rect set by `mu_layout_set_next` */
+ int type = layout->next_type;
+ layout->next_type = 0;
+ res = layout->next;
+ if (type == ABSOLUTE) { return (ctx->last_rect = res); }
+
+ } else {
+ /* handle next row */
+ if (layout->item_index == layout->items) {
+ mu_layout_row(ctx, layout->items, NULL, layout->size.y);
+ }
+
+ /* position */
+ res.x = layout->position.x;
+ res.y = layout->position.y;
+
+ /* size */
+ res.w = layout->items > 0 ? layout->widths[layout->item_index] : layout->size.x;
+ res.h = layout->size.y;
+ if (res.w == 0) { res.w = style->size.x + style->padding * 2; }
+ if (res.h == 0) { res.h = style->size.y + style->padding * 2; }
+ if (res.w < 0) { res.w += layout->body.w - res.x + 1; }
+ if (res.h < 0) { res.h += layout->body.h - res.y + 1; }
+
+ layout->item_index++;
+ }
+
+ /* update position */
+ layout->position.x += res.w + style->spacing;
+ layout->next_row = mu_max(layout->next_row, res.y + res.h + style->spacing);
+
+ /* apply body offset */
+ res.x += layout->body.x;
+ res.y += layout->body.y;
+
+ /* update max position */
+ layout->max.x = mu_max(layout->max.x, res.x + res.w);
+ layout->max.y = mu_max(layout->max.y, res.y + res.h);
+
+ return (ctx->last_rect = res);
+}
+
+
+/*============================================================================
+** controls
+**============================================================================*/
+
+static int in_hover_root(mu_Context *ctx) {
+ int i = ctx->container_stack.idx;
+ while (i--) {
+ if (ctx->container_stack.items[i] == ctx->hover_root) { return 1; }
+ /* only root containers have their `head` field set; stop searching if we've
+ ** reached the current root container */
+ if (ctx->container_stack.items[i]->head) { break; }
+ }
+ return 0;
+}
+
+
+void mu_draw_control_frame(mu_Context *ctx, mu_Id id, mu_Rect rect,
+ int colorid, int opt)
+{
+ if (opt & MU_OPT_NOFRAME) { return; }
+ colorid += (ctx->focus == id) ? 2 : (ctx->hover == id) ? 1 : 0;
+ ctx->draw_frame(ctx, rect, colorid);
+}
+
+
+void mu_draw_control_text(mu_Context *ctx, const char *str, mu_Rect rect,
+ int colorid, int opt)
+{
+ mu_Vec2 pos;
+ mu_Font font = ctx->style->font;
+ int tw = ctx->text_width(font, str, -1);
+ mu_push_clip_rect(ctx, rect);
+ pos.y = rect.y + (rect.h - ctx->text_height(font)) / 2;
+ if (opt & MU_OPT_ALIGNCENTER) {
+ pos.x = rect.x + (rect.w - tw) / 2;
+ } else if (opt & MU_OPT_ALIGNRIGHT) {
+ pos.x = rect.x + rect.w - tw - ctx->style->padding;
+ } else {
+ pos.x = rect.x + ctx->style->padding;
+ }
+ mu_draw_text(ctx, font, str, -1, pos, ctx->style->colors[colorid]);
+ mu_pop_clip_rect(ctx);
+}
+
+
+int mu_mouse_over(mu_Context *ctx, mu_Rect rect) {
+ return rect_overlaps_vec2(rect, ctx->mouse_pos) &&
+ rect_overlaps_vec2(mu_get_clip_rect(ctx), ctx->mouse_pos) &&
+ in_hover_root(ctx);
+}
+
+
+void mu_update_control(mu_Context *ctx, mu_Id id, mu_Rect rect, int opt) {
+ int mouseover = mu_mouse_over(ctx, rect);
+
+ if (ctx->focus == id) { ctx->updated_focus = 1; }
+ if (opt & MU_OPT_NOINTERACT) { return; }
+ if (mouseover && !ctx->mouse_down) { ctx->hover = id; }
+
+ if (ctx->focus == id) {
+ if (ctx->mouse_pressed && !mouseover) { mu_set_focus(ctx, 0); }
+ if (!ctx->mouse_down && ~opt & MU_OPT_HOLDFOCUS) { mu_set_focus(ctx, 0); }
+ }
+
+ if (ctx->hover == id) {
+ if (ctx->mouse_pressed) {
+ mu_set_focus(ctx, id);
+ } else if (!mouseover) {
+ ctx->hover = 0;
+ }
+ }
+}
+
+
+void mu_text(mu_Context *ctx, const char *text) {
+ const char *start, *end, *p = text;
+ int width = -1;
+ mu_Font font = ctx->style->font;
+ mu_Color color = ctx->style->colors[MU_COLOR_TEXT];
+ mu_layout_begin_column(ctx);
+ mu_layout_row(ctx, 1, &width, ctx->text_height(font));
+ do {
+ mu_Rect r = mu_layout_next(ctx);
+ int w = 0;
+ start = end = p;
+ do {
+ const char* word = p;
+ while (*p && *p != ' ' && *p != '\n') { p++; }
+ w += ctx->text_width(font, word, p - word);
+ if (w > r.w && end != start) { break; }
+ w += ctx->text_width(font, p, 1);
+ end = p++;
+ } while (*end && *end != '\n');
+ mu_draw_text(ctx, font, start, end - start, mu_vec2(r.x, r.y), color);
+ p = end + 1;
+ } while (*end);
+ mu_layout_end_column(ctx);
+}
+
+
+void mu_label(mu_Context *ctx, const char *text) {
+ mu_draw_control_text(ctx, text, mu_layout_next(ctx), MU_COLOR_TEXT, 0);
+}
+
+
+int mu_button_ex(mu_Context *ctx, const char *label, int icon, int opt) {
+ int res = 0;
+ mu_Id id = label ? mu_get_id(ctx, label, strlen(label))
+ : mu_get_id(ctx, &icon, sizeof(icon));
+ mu_Rect r = mu_layout_next(ctx);
+ mu_update_control(ctx, id, r, opt);
+ /* handle click */
+ if (ctx->mouse_pressed == MU_MOUSE_LEFT && ctx->focus == id) {
+ res |= MU_RES_SUBMIT;
+ }
+ /* draw */
+ mu_draw_control_frame(ctx, id, r, MU_COLOR_BUTTON, opt);
+ if (label) { mu_draw_control_text(ctx, label, r, MU_COLOR_TEXT, opt); }
+ if (icon) { mu_draw_icon(ctx, icon, r, ctx->style->colors[MU_COLOR_TEXT]); }
+ return res;
+}
+
+
+int mu_checkbox(mu_Context *ctx, const char *label, int *state) {
+ int res = 0;
+ mu_Id id = mu_get_id(ctx, &state, sizeof(state));
+ mu_Rect r = mu_layout_next(ctx);
+ mu_Rect box = mu_rect(r.x, r.y, r.h, r.h);
+ mu_update_control(ctx, id, r, 0);
+ /* handle click */
+ if (ctx->mouse_pressed == MU_MOUSE_LEFT && ctx->focus == id) {
+ res |= MU_RES_CHANGE;
+ *state = !*state;
+ }
+ /* draw */
+ mu_draw_control_frame(ctx, id, box, MU_COLOR_BASE, 0);
+ if (*state) {
+ mu_draw_icon(ctx, MU_ICON_CHECK, box, ctx->style->colors[MU_COLOR_TEXT]);
+ }
+ r = mu_rect(r.x + box.w, r.y, r.w - box.w, r.h);
+ mu_draw_control_text(ctx, label, r, MU_COLOR_TEXT, 0);
+ return res;
+}
+
+
+int mu_textbox_raw(mu_Context *ctx, char *buf, int bufsz, mu_Id id, mu_Rect r,
+ int opt)
+{
+ int res = 0;
+ mu_update_control(ctx, id, r, opt | MU_OPT_HOLDFOCUS);
+
+ if (ctx->focus == id) {
+ /* handle text input */
+ int len = strlen(buf);
+ int n = mu_min(bufsz - len - 1, (int) strlen(ctx->input_text));
+ if (n > 0) {
+ memcpy(buf + len, ctx->input_text, n);
+ len += n;
+ buf[len] = '\0';
+ res |= MU_RES_CHANGE;
+ }
+ /* handle backspace */
+ if (ctx->key_pressed & MU_KEY_BACKSPACE && len > 0) {
+ /* skip utf-8 continuation bytes */
+ while ((buf[--len] & 0xc0) == 0x80 && len > 0);
+ buf[len] = '\0';
+ res |= MU_RES_CHANGE;
+ }
+ /* handle return */
+ if (ctx->key_pressed & MU_KEY_RETURN) {
+ mu_set_focus(ctx, 0);
+ res |= MU_RES_SUBMIT;
+ }
+ }
+
+ /* draw */
+ mu_draw_control_frame(ctx, id, r, MU_COLOR_BASE, opt);
+ if (ctx->focus == id) {
+ mu_Color color = ctx->style->colors[MU_COLOR_TEXT];
+ mu_Font font = ctx->style->font;
+ int textw = ctx->text_width(font, buf, -1);
+ int texth = ctx->text_height(font);
+ int ofx = r.w - ctx->style->padding - textw - 1;
+ int textx = r.x + mu_min(ofx, ctx->style->padding);
+ int texty = r.y + (r.h - texth) / 2;
+ mu_push_clip_rect(ctx, r);
+ mu_draw_text(ctx, font, buf, -1, mu_vec2(textx, texty), color);
+ mu_draw_rect(ctx, mu_rect(textx + textw, texty, 1, texth), color);
+ mu_pop_clip_rect(ctx);
+ } else {
+ mu_draw_control_text(ctx, buf, r, MU_COLOR_TEXT, opt);
+ }
+
+ return res;
+}
+
+
+static int number_textbox(mu_Context *ctx, mu_Real *value, mu_Rect r, mu_Id id) {
+ if (ctx->mouse_pressed == MU_MOUSE_LEFT && ctx->key_down & MU_KEY_SHIFT &&
+ ctx->hover == id
+ ) {
+ ctx->number_edit = id;
+ sprintf(ctx->number_edit_buf, MU_REAL_FMT, *value);
+ }
+ if (ctx->number_edit == id) {
+ int res = mu_textbox_raw(
+ ctx, ctx->number_edit_buf, sizeof(ctx->number_edit_buf), id, r, 0);
+ if (res & MU_RES_SUBMIT || ctx->focus != id) {
+ *value = strtod(ctx->number_edit_buf, NULL);
+ ctx->number_edit = 0;
+ } else {
+ return 1;
+ }
+ }
+ return 0;
+}
+
+
+int mu_textbox_ex(mu_Context *ctx, char *buf, int bufsz, int opt) {
+ mu_Id id = mu_get_id(ctx, &buf, sizeof(buf));
+ mu_Rect r = mu_layout_next(ctx);
+ return mu_textbox_raw(ctx, buf, bufsz, id, r, opt);
+}
+
+
+int mu_slider_ex(mu_Context *ctx, mu_Real *value, mu_Real low, mu_Real high,
+ mu_Real step, const char *fmt, int opt)
+{
+ char buf[MU_MAX_FMT + 1];
+ mu_Rect thumb;
+ int x, w, res = 0;
+ mu_Real last = *value, v = last;
+ mu_Id id = mu_get_id(ctx, &value, sizeof(value));
+ mu_Rect base = mu_layout_next(ctx);
+
+ /* handle text input mode */
+ if (number_textbox(ctx, &v, base, id)) { return res; }
+
+ /* handle normal mode */
+ mu_update_control(ctx, id, base, opt);
+
+ /* handle input */
+ if (ctx->focus == id &&
+ (ctx->mouse_down | ctx->mouse_pressed) == MU_MOUSE_LEFT)
+ {
+ v = low + (ctx->mouse_pos.x - base.x) * (high - low) / base.w;
+ if (step) { v = ((long long)((v + step / 2) / step)) * step; }
+ }
+ /* clamp and store value, update res */
+ *value = v = mu_clamp(v, low, high);
+ if (last != v) { res |= MU_RES_CHANGE; }
+
+ /* draw base */
+ mu_draw_control_frame(ctx, id, base, MU_COLOR_BASE, opt);
+ /* draw thumb */
+ w = ctx->style->thumb_size;
+ x = (v - low) * (base.w - w) / (high - low);
+ thumb = mu_rect(base.x + x, base.y, w, base.h);
+ mu_draw_control_frame(ctx, id, thumb, MU_COLOR_BUTTON, opt);
+ /* draw text */
+ sprintf(buf, fmt, v);
+ mu_draw_control_text(ctx, buf, base, MU_COLOR_TEXT, opt);
+
+ return res;
+}
+
+
+int mu_number_ex(mu_Context *ctx, mu_Real *value, mu_Real step,
+ const char *fmt, int opt)
+{
+ char buf[MU_MAX_FMT + 1];
+ int res = 0;
+ mu_Id id = mu_get_id(ctx, &value, sizeof(value));
+ mu_Rect base = mu_layout_next(ctx);
+ mu_Real last = *value;
+
+ /* handle text input mode */
+ if (number_textbox(ctx, value, base, id)) { return res; }
+
+ /* handle normal mode */
+ mu_update_control(ctx, id, base, opt);
+
+ /* handle input */
+ if (ctx->focus == id && ctx->mouse_down == MU_MOUSE_LEFT) {
+ *value += ctx->mouse_delta.x * step;
+ }
+ /* set flag if value changed */
+ if (*value != last) { res |= MU_RES_CHANGE; }
+
+ /* draw base */
+ mu_draw_control_frame(ctx, id, base, MU_COLOR_BASE, opt);
+ /* draw text */
+ sprintf(buf, fmt, *value);
+ mu_draw_control_text(ctx, buf, base, MU_COLOR_TEXT, opt);
+
+ return res;
+}
+
+
+static int header(mu_Context *ctx, const char *label, int istreenode, int opt) {
+ mu_Rect r;
+ int active, expanded;
+ mu_Id id = mu_get_id(ctx, label, strlen(label));
+ int idx = mu_pool_get(ctx, ctx->treenode_pool, MU_TREENODEPOOL_SIZE, id);
+ int width = -1;
+ mu_layout_row(ctx, 1, &width, 0);
+
+ active = (idx >= 0);
+ expanded = (opt & MU_OPT_EXPANDED) ? !active : active;
+ r = mu_layout_next(ctx);
+ mu_update_control(ctx, id, r, 0);
+
+ /* handle click */
+ active ^= (ctx->mouse_pressed == MU_MOUSE_LEFT && ctx->focus == id);
+
+ /* update pool ref */
+ if (idx >= 0) {
+ if (active) { mu_pool_update(ctx, ctx->treenode_pool, idx); }
+ else { memset(&ctx->treenode_pool[idx], 0, sizeof(mu_PoolItem)); }
+ } else if (active) {
+ mu_pool_init(ctx, ctx->treenode_pool, MU_TREENODEPOOL_SIZE, id);
+ }
+
+ /* draw */
+ if (istreenode) {
+ if (ctx->hover == id) { ctx->draw_frame(ctx, r, MU_COLOR_BUTTONHOVER); }
+ } else {
+ mu_draw_control_frame(ctx, id, r, MU_COLOR_BUTTON, 0);
+ }
+ mu_draw_icon(
+ ctx, expanded ? MU_ICON_EXPANDED : MU_ICON_COLLAPSED,
+ mu_rect(r.x, r.y, r.h, r.h), ctx->style->colors[MU_COLOR_TEXT]);
+ r.x += r.h - ctx->style->padding;
+ r.w -= r.h - ctx->style->padding;
+ mu_draw_control_text(ctx, label, r, MU_COLOR_TEXT, 0);
+
+ return expanded ? MU_RES_ACTIVE : 0;
+}
+
+
+int mu_header_ex(mu_Context *ctx, const char *label, int opt) {
+ return header(ctx, label, 0, opt);
+}
+
+
+int mu_begin_treenode_ex(mu_Context *ctx, const char *label, int opt) {
+ int res = header(ctx, label, 1, opt);
+ if (res & MU_RES_ACTIVE) {
+ get_layout(ctx)->indent += ctx->style->indent;
+ push(ctx->id_stack, ctx->last_id);
+ }
+ return res;
+}
+
+
+void mu_end_treenode(mu_Context *ctx) {
+ get_layout(ctx)->indent -= ctx->style->indent;
+ mu_pop_id(ctx);
+}
+
+
+#define scrollbar(ctx, cnt, b, cs, x, y, w, h) \
+ do { \
+ /* only add scrollbar if content size is larger than body */ \
+ int maxscroll = cs.y - b->h; \
+ \
+ if (maxscroll > 0 && b->h > 0) { \
+ mu_Rect base, thumb; \
+ mu_Id id = mu_get_id(ctx, "!scrollbar" #y, 11); \
+ \
+ /* get sizing / positioning */ \
+ base = *b; \
+ base.x = b->x + b->w; \
+ base.w = ctx->style->scrollbar_size; \
+ \
+ /* handle input */ \
+ mu_update_control(ctx, id, base, 0); \
+ if (ctx->focus == id && ctx->mouse_down == MU_MOUSE_LEFT) { \
+ cnt->scroll.y += ctx->mouse_delta.y * cs.y / base.h; \
+ } \
+ /* clamp scroll to limits */ \
+ cnt->scroll.y = mu_clamp(cnt->scroll.y, 0, maxscroll); \
+ \
+ /* draw base and thumb */ \
+ ctx->draw_frame(ctx, base, MU_COLOR_SCROLLBASE); \
+ thumb = base; \
+ thumb.h = mu_max(ctx->style->thumb_size, base.h * b->h / cs.y); \
+ thumb.y += cnt->scroll.y * (base.h - thumb.h) / maxscroll; \
+ ctx->draw_frame(ctx, thumb, MU_COLOR_SCROLLTHUMB); \
+ \
+ /* set this as the scroll_target (will get scrolled on mousewheel) */ \
+ /* if the mouse is over it */ \
+ if (mu_mouse_over(ctx, *b)) { ctx->scroll_target = cnt; } \
+ } else { \
+ cnt->scroll.y = 0; \
+ } \
+ } while (0)
+
+
+static void scrollbars(mu_Context *ctx, mu_Container *cnt, mu_Rect *body) {
+ int sz = ctx->style->scrollbar_size;
+ mu_Vec2 cs = cnt->content_size;
+ cs.x += ctx->style->padding * 2;
+ cs.y += ctx->style->padding * 2;
+ mu_push_clip_rect(ctx, *body);
+ /* resize body to make room for scrollbars */
+ if (cs.y > cnt->body.h) { body->w -= sz; }
+ if (cs.x > cnt->body.w) { body->h -= sz; }
+ /* to create a horizontal or vertical scrollbar almost-identical code is
+ ** used; only the references to `x|y` `w|h` need to be switched */
+ scrollbar(ctx, cnt, body, cs, x, y, w, h);
+ scrollbar(ctx, cnt, body, cs, y, x, h, w);
+ mu_pop_clip_rect(ctx);
+}
+
+
+static void push_container_body(
+ mu_Context *ctx, mu_Container *cnt, mu_Rect body, int opt
+) {
+ if (~opt & MU_OPT_NOSCROLL) { scrollbars(ctx, cnt, &body); }
+ push_layout(ctx, expand_rect(body, -ctx->style->padding), cnt->scroll);
+ cnt->body = body;
+}
+
+
+static void begin_root_container(mu_Context *ctx, mu_Container *cnt) {
+ push(ctx->container_stack, cnt);
+ /* push container to roots list and push head command */
+ push(ctx->root_list, cnt);
+ cnt->head = push_jump(ctx, NULL);
+ /* set as hover root if the mouse is overlapping this container and it has a
+ ** higher zindex than the current hover root */
+ if (rect_overlaps_vec2(cnt->rect, ctx->mouse_pos) &&
+ (!ctx->next_hover_root || cnt->zindex > ctx->next_hover_root->zindex)
+ ) {
+ ctx->next_hover_root = cnt;
+ }
+ /* clipping is reset here in case a root-container is made within
+ ** another root-containers's begin/end block; this prevents the inner
+ ** root-container being clipped to the outer */
+ push(ctx->clip_stack, unclipped_rect);
+}
+
+
+static void end_root_container(mu_Context *ctx) {
+ /* push tail 'goto' jump command and set head 'skip' command. the final steps
+ ** on initing these are done in mu_end() */
+ mu_Container *cnt = mu_get_current_container(ctx);
+ cnt->tail = push_jump(ctx, NULL);
+ cnt->head->jump.dst = ctx->command_list.items + ctx->command_list.idx;
+ /* pop base clip rect and container */
+ mu_pop_clip_rect(ctx);
+ pop_container(ctx);
+}
+
+
+int mu_begin_window_ex(mu_Context *ctx, const char *title, mu_Rect rect, int opt) {
+ mu_Rect body;
+ mu_Id id = mu_get_id(ctx, title, strlen(title));
+ mu_Container *cnt = get_container(ctx, id, opt);
+ if (!cnt || !cnt->open) { return 0; }
+ push(ctx->id_stack, id);
+
+ if (cnt->rect.w == 0) { cnt->rect = rect; }
+ begin_root_container(ctx, cnt);
+ rect = body = cnt->rect;
+
+ /* draw frame */
+ if (~opt & MU_OPT_NOFRAME) {
+ ctx->draw_frame(ctx, rect, MU_COLOR_WINDOWBG);
+ }
+
+ /* do title bar */
+ if (~opt & MU_OPT_NOTITLE) {
+ mu_Rect tr = rect;
+ tr.h = ctx->style->title_height;
+ ctx->draw_frame(ctx, tr, MU_COLOR_TITLEBG);
+
+ /* do title text */
+ if (~opt & MU_OPT_NOTITLE) {
+ mu_Id id = mu_get_id(ctx, "!title", 6);
+ mu_update_control(ctx, id, tr, opt);
+ mu_draw_control_text(ctx, title, tr, MU_COLOR_TITLETEXT, opt);
+ if (id == ctx->focus && ctx->mouse_down == MU_MOUSE_LEFT) {
+ cnt->rect.x += ctx->mouse_delta.x;
+ cnt->rect.y += ctx->mouse_delta.y;
+#ifdef MU_N64_MODIFICATIONS
+ if(cnt->rect.x < 0)cnt->rect.x = 0;
+ if(cnt->rect.y < 0)cnt->rect.y = 0;
+#endif
+ }
+ body.y += tr.h;
+ body.h -= tr.h;
+ }
+
+ /* do `close` button */
+ if (~opt & MU_OPT_NOCLOSE) {
+ mu_Id id = mu_get_id(ctx, "!close", 6);
+ mu_Rect r = mu_rect(tr.x + tr.w - tr.h, tr.y, tr.h, tr.h);
+ tr.w -= r.w;
+ mu_draw_icon(ctx, MU_ICON_CLOSE, r, ctx->style->colors[MU_COLOR_TITLETEXT]);
+ mu_update_control(ctx, id, r, opt);
+ if (ctx->mouse_pressed == MU_MOUSE_LEFT && id == ctx->focus) {
+ cnt->open = 0;
+ }
+ }
+ }
+
+ push_container_body(ctx, cnt, body, opt);
+
+ /* do `resize` handle */
+ if (~opt & MU_OPT_NORESIZE) {
+ int sz = ctx->style->title_height;
+ mu_Id id = mu_get_id(ctx, "!resize", 7);
+ mu_Rect r = mu_rect(rect.x + rect.w - sz, rect.y + rect.h - sz, sz, sz);
+ mu_update_control(ctx, id, r, opt);
+ if (id == ctx->focus && ctx->mouse_down == MU_MOUSE_LEFT) {
+#ifdef MU_N64_MODIFICATIONS
+ cnt->rect.w = mu_max(48, cnt->rect.w + ctx->mouse_delta.x);
+ cnt->rect.h = mu_max(32, cnt->rect.h + ctx->mouse_delta.y);
+#else
+ cnt->rect.w = mu_max(96, cnt->rect.w + ctx->mouse_delta.x);
+ cnt->rect.h = mu_max(64, cnt->rect.h + ctx->mouse_delta.y);
+#endif
+ }
+ }
+
+ /* resize to content size */
+ if (opt & MU_OPT_AUTOSIZE) {
+ mu_Rect r = get_layout(ctx)->body;
+ cnt->rect.w = cnt->content_size.x + (cnt->rect.w - r.w);
+ cnt->rect.h = cnt->content_size.y + (cnt->rect.h - r.h);
+ }
+
+ /* close if this is a popup window and elsewhere was clicked */
+ if (opt & MU_OPT_POPUP && ctx->mouse_pressed && ctx->hover_root != cnt) {
+ cnt->open = 0;
+ }
+
+ mu_push_clip_rect(ctx, cnt->body);
+ return MU_RES_ACTIVE;
+}
+
+
+void mu_end_window(mu_Context *ctx) {
+ mu_pop_clip_rect(ctx);
+ end_root_container(ctx);
+}
+
+
+void mu_open_popup(mu_Context *ctx, const char *name) {
+ mu_Container *cnt = mu_get_container(ctx, name);
+ /* set as hover root so popup isn't closed in begin_window_ex() */
+ ctx->hover_root = ctx->next_hover_root = cnt;
+ /* position at mouse cursor, open and bring-to-front */
+ cnt->rect = mu_rect(ctx->mouse_pos.x, ctx->mouse_pos.y, 1, 1);
+ cnt->open = 1;
+ mu_bring_to_front(ctx, cnt);
+}
+
+
+int mu_begin_popup(mu_Context *ctx, const char *name) {
+ int opt = MU_OPT_POPUP | MU_OPT_AUTOSIZE | MU_OPT_NORESIZE |
+ MU_OPT_NOSCROLL | MU_OPT_NOTITLE | MU_OPT_CLOSED;
+ return mu_begin_window_ex(ctx, name, mu_rect(0, 0, 0, 0), opt);
+}
+
+
+void mu_end_popup(mu_Context *ctx) {
+ mu_end_window(ctx);
+}
+
+
+void mu_begin_panel_ex(mu_Context *ctx, const char *name, int opt) {
+ mu_Container *cnt;
+ mu_push_id(ctx, name, strlen(name));
+ cnt = get_container(ctx, ctx->last_id, opt);
+ cnt->rect = mu_layout_next(ctx);
+ if (~opt & MU_OPT_NOFRAME) {
+ ctx->draw_frame(ctx, cnt->rect, MU_COLOR_PANELBG);
+ }
+ push(ctx->container_stack, cnt);
+ push_container_body(ctx, cnt, cnt->rect, opt);
+ mu_push_clip_rect(ctx, cnt->body);
+}
+
+
+void mu_end_panel(mu_Context *ctx) {
+ mu_pop_clip_rect(ctx);
+ pop_container(ctx);
+}
diff --git a/examples/micro-ui/lib/microui.h b/examples/micro-ui/lib/microui.h
new file mode 100644
index 000000000..01a741034
--- /dev/null
+++ b/examples/micro-ui/lib/microui.h
@@ -0,0 +1,314 @@
+/*
+** Copyright (c) 2024 rxi
+**
+** This library is free software; you can redistribute it and/or modify it
+** under the terms of the MIT license. See `microui.c` for details.
+*/
+
+#ifndef MICROUI_H
+#define MICROUI_H
+
+#define MU_VERSION "2.02"
+
+#define MU_N64_MODIFICATIONS
+
+#define MU_COMMANDLIST_SIZE (256 * 1024)
+#define MU_ROOTLIST_SIZE 32
+#define MU_CONTAINERSTACK_SIZE 32
+#define MU_CLIPSTACK_SIZE 32
+#define MU_IDSTACK_SIZE 32
+#define MU_LAYOUTSTACK_SIZE 16
+#define MU_CONTAINERPOOL_SIZE 48
+#define MU_TREENODEPOOL_SIZE 48
+#define MU_MAX_WIDTHS 16
+#define MU_REAL float
+#define MU_REAL_FMT "%.3g"
+#define MU_SLIDER_FMT "%.2f"
+#define MU_MAX_FMT 127
+
+#define mu_stack(T, n) struct { int idx; T items[n]; }
+#define mu_min(a, b) ((a) < (b) ? (a) : (b))
+#define mu_max(a, b) ((a) > (b) ? (a) : (b))
+#define mu_clamp(x, a, b) mu_min(b, mu_max(a, x))
+
+enum {
+ MU_CLIP_PART = 1,
+ MU_CLIP_ALL
+};
+
+enum {
+ MU_COMMAND_JUMP = 1,
+ MU_COMMAND_CLIP,
+ MU_COMMAND_RECT,
+ MU_COMMAND_TEXT,
+ MU_COMMAND_ICON,
+#ifdef MU_N64_MODIFICATIONS
+ MU_COMMAND_SURFACE,
+ MU_COMMAND_SPRITE,
+#endif
+ MU_COMMAND_MAX
+};
+
+enum {
+ MU_COLOR_TEXT,
+ MU_COLOR_BORDER,
+ MU_COLOR_WINDOWBG,
+ MU_COLOR_TITLEBG,
+ MU_COLOR_TITLETEXT,
+ MU_COLOR_PANELBG,
+ MU_COLOR_BUTTON,
+ MU_COLOR_BUTTONHOVER,
+ MU_COLOR_BUTTONFOCUS,
+ MU_COLOR_BASE,
+ MU_COLOR_BASEHOVER,
+ MU_COLOR_BASEFOCUS,
+ MU_COLOR_SCROLLBASE,
+ MU_COLOR_SCROLLTHUMB,
+ MU_COLOR_MAX
+};
+
+enum {
+ MU_ICON_CLOSE = 1,
+ MU_ICON_CHECK,
+ MU_ICON_COLLAPSED,
+ MU_ICON_EXPANDED,
+ MU_ICON_MAX
+};
+
+enum {
+ MU_RES_ACTIVE = (1 << 0),
+ MU_RES_SUBMIT = (1 << 1),
+ MU_RES_CHANGE = (1 << 2)
+};
+
+enum {
+ MU_OPT_ALIGNCENTER = (1 << 0),
+ MU_OPT_ALIGNRIGHT = (1 << 1),
+ MU_OPT_NOINTERACT = (1 << 2),
+ MU_OPT_NOFRAME = (1 << 3),
+ MU_OPT_NORESIZE = (1 << 4),
+ MU_OPT_NOSCROLL = (1 << 5),
+ MU_OPT_NOCLOSE = (1 << 6),
+ MU_OPT_NOTITLE = (1 << 7),
+ MU_OPT_HOLDFOCUS = (1 << 8),
+ MU_OPT_AUTOSIZE = (1 << 9),
+ MU_OPT_POPUP = (1 << 10),
+ MU_OPT_CLOSED = (1 << 11),
+ MU_OPT_EXPANDED = (1 << 12)
+};
+
+enum {
+ MU_MOUSE_LEFT = (1 << 0),
+ MU_MOUSE_RIGHT = (1 << 1),
+ MU_MOUSE_MIDDLE = (1 << 2)
+};
+
+enum {
+ MU_KEY_SHIFT = (1 << 0),
+ MU_KEY_CTRL = (1 << 1),
+ MU_KEY_ALT = (1 << 2),
+ MU_KEY_BACKSPACE = (1 << 3),
+ MU_KEY_RETURN = (1 << 4)
+};
+
+
+typedef struct mu_Context mu_Context;
+typedef unsigned mu_Id;
+typedef MU_REAL mu_Real;
+typedef void* mu_Font;
+
+typedef struct { int x, y; } mu_Vec2;
+typedef struct { int x, y, w, h; } mu_Rect;
+typedef struct { unsigned char r, g, b, a; } mu_Color;
+typedef struct { mu_Id id; int last_update; } mu_PoolItem;
+
+typedef struct { int type, size; } mu_BaseCommand;
+typedef struct { mu_BaseCommand base; void *dst; } mu_JumpCommand;
+typedef struct { mu_BaseCommand base; mu_Rect rect; } mu_ClipCommand;
+typedef struct { mu_BaseCommand base; mu_Rect rect; mu_Color color; } mu_RectCommand;
+typedef struct { mu_BaseCommand base; mu_Font font; mu_Vec2 pos; mu_Color color; char str[1]; } mu_TextCommand;
+typedef struct { mu_BaseCommand base; mu_Rect rect; int id; mu_Color color; } mu_IconCommand;
+#ifdef MU_N64_MODIFICATIONS
+typedef struct { mu_BaseCommand base; mu_Rect rect; const void* surface; } mu_SurfaceCommand;
+typedef struct { mu_BaseCommand base; mu_Rect rect; const void* sprite; } mu_SpriteCommand;
+#endif
+
+typedef union {
+ int type;
+ mu_BaseCommand base;
+ mu_JumpCommand jump;
+ mu_ClipCommand clip;
+ mu_RectCommand rect;
+ mu_TextCommand text;
+ mu_IconCommand icon;
+#ifdef MU_N64_MODIFICATIONS
+ mu_SurfaceCommand surface;
+ mu_SpriteCommand sprite;
+#endif
+} mu_Command;
+
+typedef struct {
+ mu_Rect body;
+ mu_Rect next;
+ mu_Vec2 position;
+ mu_Vec2 size;
+ mu_Vec2 max;
+ int widths[MU_MAX_WIDTHS];
+ int items;
+ int item_index;
+ int next_row;
+ int next_type;
+ int indent;
+} mu_Layout;
+
+typedef struct {
+ mu_Command *head, *tail;
+ mu_Rect rect;
+ mu_Rect body;
+ mu_Vec2 content_size;
+ mu_Vec2 scroll;
+ int zindex;
+ int open;
+} mu_Container;
+
+typedef struct {
+ mu_Font font;
+ mu_Vec2 size;
+ int padding;
+ int spacing;
+ int indent;
+ int title_height;
+ int scrollbar_size;
+ int thumb_size;
+ mu_Color colors[MU_COLOR_MAX];
+} mu_Style;
+
+struct mu_Context {
+ /* callbacks */
+ int (*text_width)(mu_Font font, const char *str, int len);
+ int (*text_height)(mu_Font font);
+ void (*draw_frame)(mu_Context *ctx, mu_Rect rect, int colorid);
+ /* core state */
+ mu_Style _style;
+ mu_Style *style;
+ mu_Id hover;
+ mu_Id focus;
+ mu_Id last_id;
+ mu_Rect last_rect;
+ int last_zindex;
+ int updated_focus;
+ int frame;
+ mu_Container *hover_root;
+ mu_Container *next_hover_root;
+ mu_Container *scroll_target;
+ char number_edit_buf[MU_MAX_FMT];
+ mu_Id number_edit;
+ /* stacks */
+ mu_stack(char, MU_COMMANDLIST_SIZE) command_list;
+ mu_stack(mu_Container*, MU_ROOTLIST_SIZE) root_list;
+ mu_stack(mu_Container*, MU_CONTAINERSTACK_SIZE) container_stack;
+ mu_stack(mu_Rect, MU_CLIPSTACK_SIZE) clip_stack;
+ mu_stack(mu_Id, MU_IDSTACK_SIZE) id_stack;
+ mu_stack(mu_Layout, MU_LAYOUTSTACK_SIZE) layout_stack;
+ /* retained state pools */
+ mu_PoolItem container_pool[MU_CONTAINERPOOL_SIZE];
+ mu_Container containers[MU_CONTAINERPOOL_SIZE];
+ mu_PoolItem treenode_pool[MU_TREENODEPOOL_SIZE];
+ /* input state */
+ mu_Vec2 mouse_pos;
+ mu_Vec2 last_mouse_pos;
+ mu_Vec2 mouse_delta;
+ mu_Vec2 scroll_delta;
+ int mouse_down;
+ int mouse_pressed;
+ int key_down;
+ int key_pressed;
+ char input_text[32];
+};
+
+
+mu_Vec2 mu_vec2(int x, int y);
+mu_Rect mu_rect(int x, int y, int w, int h);
+mu_Color mu_color(int r, int g, int b, int a);
+
+void mu_init(mu_Context *ctx);
+void mu_begin(mu_Context *ctx);
+void mu_end(mu_Context *ctx);
+void mu_set_focus(mu_Context *ctx, mu_Id id);
+mu_Id mu_get_id(mu_Context *ctx, const void *data, int size);
+void mu_push_id(mu_Context *ctx, const void *data, int size);
+void mu_pop_id(mu_Context *ctx);
+void mu_push_clip_rect(mu_Context *ctx, mu_Rect rect);
+void mu_pop_clip_rect(mu_Context *ctx);
+mu_Rect mu_get_clip_rect(mu_Context *ctx);
+int mu_check_clip(mu_Context *ctx, mu_Rect r);
+mu_Container* mu_get_current_container(mu_Context *ctx);
+mu_Container* mu_get_container(mu_Context *ctx, const char *name);
+void mu_bring_to_front(mu_Context *ctx, mu_Container *cnt);
+
+int mu_pool_init(mu_Context *ctx, mu_PoolItem *items, int len, mu_Id id);
+int mu_pool_get(mu_Context *ctx, mu_PoolItem *items, int len, mu_Id id);
+void mu_pool_update(mu_Context *ctx, mu_PoolItem *items, int idx);
+
+void mu_input_mousemove(mu_Context *ctx, int x, int y);
+void mu_input_mousedown(mu_Context *ctx, int x, int y, int btn);
+void mu_input_mouseup(mu_Context *ctx, int x, int y, int btn);
+void mu_input_scroll(mu_Context *ctx, int x, int y);
+void mu_input_keydown(mu_Context *ctx, int key);
+void mu_input_keyup(mu_Context *ctx, int key);
+void mu_input_text(mu_Context *ctx, const char *text);
+
+mu_Command* mu_push_command(mu_Context *ctx, int type, int size);
+int mu_next_command(mu_Context *ctx, mu_Command **cmd);
+void mu_set_clip(mu_Context *ctx, mu_Rect rect);
+void mu_draw_rect(mu_Context *ctx, mu_Rect rect, mu_Color color);
+void mu_draw_box(mu_Context *ctx, mu_Rect rect, mu_Color color);
+void mu_draw_text(mu_Context *ctx, mu_Font font, const char *str, int len, mu_Vec2 pos, mu_Color color);
+void mu_draw_icon(mu_Context *ctx, int id, mu_Rect rect, mu_Color color);
+#ifdef MU_N64_MODIFICATIONS
+void mu_draw_surface(mu_Context *ctx, const void* surf, mu_Rect rect);
+void mu_draw_sprite(mu_Context *ctx, const void* sprite, mu_Rect rect);
+#endif
+
+void mu_layout_row(mu_Context *ctx, int items, const int *widths, int height);
+void mu_layout_width(mu_Context *ctx, int width);
+void mu_layout_height(mu_Context *ctx, int height);
+void mu_layout_begin_column(mu_Context *ctx);
+void mu_layout_end_column(mu_Context *ctx);
+void mu_layout_set_next(mu_Context *ctx, mu_Rect r, int relative);
+mu_Rect mu_layout_next(mu_Context *ctx);
+
+void mu_draw_control_frame(mu_Context *ctx, mu_Id id, mu_Rect rect, int colorid, int opt);
+void mu_draw_control_text(mu_Context *ctx, const char *str, mu_Rect rect, int colorid, int opt);
+int mu_mouse_over(mu_Context *ctx, mu_Rect rect);
+void mu_update_control(mu_Context *ctx, mu_Id id, mu_Rect rect, int opt);
+
+#define mu_button(ctx, label) mu_button_ex(ctx, label, 0, MU_OPT_ALIGNCENTER)
+#define mu_textbox(ctx, buf, bufsz) mu_textbox_ex(ctx, buf, bufsz, 0)
+#define mu_slider(ctx, value, lo, hi) mu_slider_ex(ctx, value, lo, hi, 0, MU_SLIDER_FMT, MU_OPT_ALIGNCENTER)
+#define mu_number(ctx, value, step) mu_number_ex(ctx, value, step, MU_SLIDER_FMT, MU_OPT_ALIGNCENTER)
+#define mu_header(ctx, label) mu_header_ex(ctx, label, 0)
+#define mu_begin_treenode(ctx, label) mu_begin_treenode_ex(ctx, label, 0)
+#define mu_begin_window(ctx, title, rect) mu_begin_window_ex(ctx, title, rect, 0)
+#define mu_begin_panel(ctx, name) mu_begin_panel_ex(ctx, name, 0)
+
+void mu_text(mu_Context *ctx, const char *text);
+void mu_label(mu_Context *ctx, const char *text);
+int mu_button_ex(mu_Context *ctx, const char *label, int icon, int opt);
+int mu_checkbox(mu_Context *ctx, const char *label, int *state);
+int mu_textbox_raw(mu_Context *ctx, char *buf, int bufsz, mu_Id id, mu_Rect r, int opt);
+int mu_textbox_ex(mu_Context *ctx, char *buf, int bufsz, int opt);
+int mu_slider_ex(mu_Context *ctx, mu_Real *value, mu_Real low, mu_Real high, mu_Real step, const char *fmt, int opt);
+int mu_number_ex(mu_Context *ctx, mu_Real *value, mu_Real step, const char *fmt, int opt);
+int mu_header_ex(mu_Context *ctx, const char *label, int opt);
+int mu_begin_treenode_ex(mu_Context *ctx, const char *label, int opt);
+void mu_end_treenode(mu_Context *ctx);
+int mu_begin_window_ex(mu_Context *ctx, const char *title, mu_Rect rect, int opt);
+void mu_end_window(mu_Context *ctx);
+void mu_open_popup(mu_Context *ctx, const char *name);
+int mu_begin_popup(mu_Context *ctx, const char *name);
+void mu_end_popup(mu_Context *ctx);
+void mu_begin_panel_ex(mu_Context *ctx, const char *name, int opt);
+void mu_end_panel(mu_Context *ctx);
+
+#endif
diff --git a/examples/micro-ui/lib/microuiN64.c b/examples/micro-ui/lib/microuiN64.c
new file mode 100644
index 000000000..eba860582
--- /dev/null
+++ b/examples/micro-ui/lib/microuiN64.c
@@ -0,0 +1,212 @@
+/**
+* @copyright 2023 - Max Bebök
+* @license TBD
+*/
+
+#include "microuiN64.h"
+#include "libdragon.h"
+
+#define FONT_SIZE 8
+#define TILE_WIDTH 10
+
+mu_Context mu_ctx;
+
+static sprite_t *sprite = NULL;
+static uint8_t font_index = 0;
+static joypad_port_t joypad_index = 0;
+
+static float mouse_pos_raw[2] = {40,40};
+static int mouse_pos[2] = {40,40};
+static bool cursor_active = true;
+static float cursor_speed = 0.025f;
+static float n64_mouse_speed = 200.0f;
+static bool is_n64_mouse = false;
+
+// Approx. for text size
+static int text_width(mu_Font fnt, const char *text, int len) {
+ if (len == -1) { len = strlen(text); }
+ return len * (FONT_SIZE-3);
+}
+
+static int text_height(mu_Font fnt) {
+ return FONT_SIZE;
+}
+
+bool mu64_is_active()
+{
+ return cursor_active;
+}
+
+void mu64_set_mouse_speed(float speed)
+{
+ cursor_speed = speed;
+}
+
+void mu64_init(joypad_port_t joypad_idx, uint8_t font_idx)
+{
+ joypad_index = joypad_idx;
+ font_index = font_idx;
+ if(!sprite) {
+ sprite = sprite_load("rom:/mui.sprite");
+ }
+
+ mu_init(&mu_ctx);
+ mu_ctx.text_width = text_width;
+ mu_ctx.text_height = text_height;
+ mu_ctx._style.padding = 1;
+ mu_ctx._style.title_height = 12;
+ mu_ctx._style.spacing = 1;
+ mu_ctx._style.indent = 6;
+ mu_ctx._style.thumb_size = 8;
+ mu_ctx._style.colors[MU_COLOR_TITLEBG] = (mu_Color){0x1c, 0x4f, 0x97, 0xFF};
+ mu_ctx._style.colors[MU_COLOR_BORDER] = (mu_Color){0x10, 0x10, 0x10, 0xFF};
+
+ cursor_active = true;
+ is_n64_mouse = joybus_get_identifier(joypad_index, NULL) == JOYBUS_IDENTIFIER_N64_MOUSE;
+}
+
+void mu64_start_frame()
+{
+ joypad_buttons_t btn_press = joypad_get_buttons_pressed(joypad_index);
+ if(btn_press.l) {
+ cursor_active = !cursor_active;
+ }
+
+ if(cursor_active) {
+ joypad_inputs_t mouse = joypad_get_inputs(joypad_index);
+ joypad_buttons_t btn_release = joypad_get_buttons_released(joypad_index);
+
+ float delta_x = (float)(mouse.stick_x);
+ float delta_y = (float)(mouse.stick_y);
+
+ if(is_n64_mouse) {
+ delta_x *= n64_mouse_speed;
+ delta_y *= n64_mouse_speed;
+ } else {
+ delta_x = fabsf(delta_x) * delta_x;
+ delta_y = fabsf(delta_y) * delta_y;
+ }
+
+ mouse_pos_raw[0] += delta_x * cursor_speed;
+ mouse_pos_raw[1] -= delta_y * cursor_speed;
+
+ mouse_pos_raw[0] = fmaxf(0.0f, fminf(mouse_pos_raw[0], display_get_width()-8.0f));
+ mouse_pos_raw[1] = fmaxf(0.0f, fminf(mouse_pos_raw[1], display_get_height()-8.0f));
+
+ mouse_pos[0] = (int)mouse_pos_raw[0];
+ mouse_pos[1] = (int)mouse_pos_raw[1];
+
+ if(btn_press.a) {
+ mu_input_mousedown(&mu_ctx, mouse_pos[0], mouse_pos[1], MU_MOUSE_LEFT);
+ } else if(btn_release.a) {
+ mu_input_mouseup(&mu_ctx, mouse_pos[0], mouse_pos[1], MU_MOUSE_LEFT);
+ } else {
+ mu_input_mousemove(&mu_ctx, mouse_pos[0], mouse_pos[1]);
+ }
+ }
+
+ mu_begin(&mu_ctx);
+}
+
+void mu64_end_frame()
+{
+ mu_end(&mu_ctx);
+}
+
+void mu64_draw()
+{
+ if(!cursor_active)return;
+
+ rdpq_textparms_t txt_param = {};
+ rdpq_blitparms_t tile_param = {.width = TILE_WIDTH};
+
+ rdpq_set_mode_standard();
+
+ mu_Command *cmd = NULL;
+ while (mu_next_command(&mu_ctx, &cmd))
+ {
+ switch (cmd->type) {
+ case MU_COMMAND_TEXT:
+ rdpq_text_print(&txt_param, font_index, cmd->text.pos.x, cmd->text.pos.y + FONT_SIZE - 1, cmd->text.str);
+ break;
+
+ case MU_COMMAND_RECT:
+ if(cmd->rect.color.a != 0) {
+ rdpq_set_mode_fill(*(color_t*)(void*)&cmd->rect.color);
+ rdpq_fill_rectangle(
+ cmd->rect.rect.x, cmd->rect.rect.y,
+ cmd->rect.rect.x + cmd->rect.rect.w,
+ cmd->rect.rect.y + cmd->rect.rect.h
+ );
+ }
+ break;
+
+ case MU_COMMAND_ICON:
+ if(cmd->icon.id > 0) {
+ rdpq_set_mode_standard();
+ rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY);
+ tile_param.s0 = (cmd->icon.id-1) * TILE_WIDTH;
+ rdpq_sprite_blit(sprite, cmd->icon.rect.x, cmd->icon.rect.y, &tile_param);
+ }
+ break;
+
+ case MU_COMMAND_SURFACE:
+ if(cmd->surface.surface) {
+ rdpq_set_mode_standard();
+ rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY);
+
+ surface_t *surface = (surface_t*)cmd->surface.surface;
+ rdpq_blitparms_t blitParam = {
+ .width = surface->width,
+ .height = surface->height,
+ .scale_x = (float)cmd->surface.rect.w / (float)surface->width,
+ .scale_y = (float)cmd->surface.rect.h / (float)surface->height,
+ };
+ rdpq_tex_blit(surface, cmd->surface.rect.x, cmd->surface.rect.y, &blitParam);
+ }
+ break;
+
+ case MU_COMMAND_SPRITE:
+ if(cmd->surface.surface) {
+ rdpq_set_mode_standard();
+ rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY);
+
+ sprite_t *sprite = (sprite_t*)cmd->sprite.sprite;
+ rdpq_blitparms_t blitParam = {
+ .width = sprite->width,
+ .height = sprite->height,
+ .scale_x = (float)cmd->sprite.rect.w / (float)sprite->width,
+ .scale_y = (float)cmd->sprite.rect.h / (float)sprite->height,
+ };
+ rdpq_sprite_blit(sprite, cmd->sprite.rect.x, cmd->sprite.rect.y, &blitParam);
+ }
+ break;
+
+ case MU_COMMAND_CLIP:
+ int clipEndX = cmd->clip.rect.x + cmd->clip.rect.w;
+ int clipEndY = cmd->clip.rect.y + cmd->clip.rect.h;
+ rdpq_set_scissor(
+ cmd->clip.rect.x, cmd->clip.rect.y,
+ clipEndX > display_get_width() ? display_get_width() : clipEndX,
+ clipEndY > display_get_height() ? display_get_height() : clipEndY
+ );
+
+ break;
+ }
+ }
+
+ rdpq_set_scissor(0, 0, display_get_width(), display_get_height());
+
+ if(cursor_active) {
+ rdpq_set_mode_standard();
+ rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY);
+
+ tile_param.s0 = (MU_ICON_MAX-1) * TILE_WIDTH;
+ tile_param.scale_x = mu_ctx.mouse_down ? 0.8f : 1.0f;
+ tile_param.scale_y = tile_param.scale_x;
+ rdpq_sprite_blit(sprite, mouse_pos_raw[0], mouse_pos_raw[1], &tile_param);
+ tile_param.scale_x = 1.0f;
+ tile_param.scale_y = 1.0f;
+ }
+}
+
diff --git a/examples/micro-ui/lib/microuiN64.h b/examples/micro-ui/lib/microuiN64.h
new file mode 100644
index 000000000..1b3461750
--- /dev/null
+++ b/examples/micro-ui/lib/microuiN64.h
@@ -0,0 +1,20 @@
+#ifndef H_MICROUI_N64
+#define H_MICROUI_N64
+
+#include "microui.h"
+#include
+#include
+#include
+
+void mu64_init(joypad_port_t joypad_idx, uint8_t font_idx);
+
+void mu64_start_frame();
+void mu64_end_frame();
+void mu64_draw();
+
+bool mu64_is_active();
+void mu64_set_mouse_speed(float speed);
+
+extern mu_Context mu_ctx;
+
+#endif
\ No newline at end of file
diff --git a/examples/micro-ui/micro-ui.c b/examples/micro-ui/micro-ui.c
new file mode 100644
index 000000000..9d7d457ca
--- /dev/null
+++ b/examples/micro-ui/micro-ui.c
@@ -0,0 +1,302 @@
+/**
+ * NOTE:
+ * For necessary implementation steps, look at the "Step X/5" comments in this file.
+ */
+
+#include "libdragon.h"
+#include
+#include
+#include
+
+#include "lib/microuiN64.h"
+#include "demo-helper.h"
+
+#define DEBUG_RDP 0
+
+static int dpl_cube;
+static float clear_color[3] = {0.05f, 0.05f, 0.05f};
+static float hue = 0.42f;
+static float box_rot = 0.0f;
+
+#define SPRITE_COUNT 4
+static char *sprite_names[SPRITE_COUNT] = {"test_1m", "test_2m", "test_5m", "test_10m"};
+static sprite_t *sprites[SPRITE_COUNT] = {NULL, NULL, NULL, NULL};
+
+static bool cube_visible = true;
+static int capture_cube = 1;
+static mu_Rect cube_win_rect = (mu_Rect){114, 110, 80, 80};
+static int capture_screen = 1;
+static int capture_count = 1;
+
+static const surface_t *last_surface = NULL;
+static uint32_t last_time_ms = 0;
+static float time_delta_ms = 1000.0 / 60.0f;
+
+void game_update()
+{
+ box_rot += 0.7f;
+
+ mu64_set_mouse_speed(0.00004f * time_delta_ms); // keep cursor speed constant
+
+ // You can create windows at any point in your game-logic.
+ // This does not render the window directly, which is handled later in a single batch.
+
+ // Basic window, you can add inputs to modify variables
+ if (mu_begin_window_ex(&mu_ctx, "Settings", mu_rect(12, 20, 90, 140), MU_OPT_NOCLOSE))
+ {
+ mu_layout_row(&mu_ctx, 1, (int[]) { -1 }, 0);
+ mu_label(&mu_ctx, "Background");
+
+ if(mu_slider(&mu_ctx, &hue, 0.0f, 1.0f)) {
+ rainbow(hue, &clear_color[0], &clear_color[1], &clear_color[2]);
+ }
+
+ if(mu_button(&mu_ctx, "Remove Screen")) {
+ if(capture_count > 0)--capture_count;
+ }
+ if(mu_button(&mu_ctx, "Add Screen")) {
+ ++capture_count;
+ }
+
+ if (mu_header_ex(&mu_ctx, "Time", MU_OPT_EXPANDED)) {
+ float fps = display_get_fps();
+
+ char fps_buffer[16] = {0};
+ sprintf(fps_buffer, "FPS: %.4f", fps);
+ mu_label(&mu_ctx, fps_buffer);
+
+ sprintf(fps_buffer, "ms: %.4f", time_delta_ms);
+ mu_label(&mu_ctx, fps_buffer);
+ }
+
+ mu_end_window(&mu_ctx);
+ }
+
+ // you can also temp. changes styles and request information about the window itself
+ mu_Color oldColor = mu_ctx._style.colors[MU_COLOR_WINDOWBG];
+ if(capture_cube) {
+ mu_ctx._style.colors[MU_COLOR_WINDOWBG] = (mu_Color){0,0,0,0};
+ }
+
+ if (mu_begin_window_ex(&mu_ctx, "3D-Cube", cube_win_rect, 0))
+ {
+ mu_draw_rect(&mu_ctx, (mu_Rect){cube_win_rect.x, cube_win_rect.y, cube_win_rect.w, 26}, oldColor);
+
+ mu_layout_row(&mu_ctx, 1, (int[]) { -1 }, 0);
+ mu_checkbox(&mu_ctx, "In Window", &capture_cube);
+ mu_end_window(&mu_ctx);
+ }
+
+ mu_ctx._style.colors[MU_COLOR_WINDOWBG] = oldColor;
+
+ mu_Container *cube_cont = mu_get_container(&mu_ctx, "3D-Cube");
+ cube_visible = cube_cont->open;
+ cube_win_rect = cube_cont->rect;
+
+ // Dynamic windows created in a loop:
+ for(int i=0; irect.x, cont->rect.y + menu_height,
+ cont->rect.w, cont->rect.h - menu_height
+ });
+ }
+
+ mu_end_window(&mu_ctx);
+ }
+ }
+
+ // Trees: can be fixed or dynamically created
+ // Popups: opens a new temp. window at the cursor, closes on clicking somewhere else
+ if (mu_begin_window_ex(&mu_ctx, "Files", (mu_Rect){208, 20, 100, 100}, 0))
+ {
+ mu_layout_row(&mu_ctx, 1, (int[]) { -1 }, 0);
+ char sprite_text_buff[32];
+ int old_indent = mu_ctx._style.indent;
+
+ if (mu_begin_treenode(&mu_ctx, "ROM"))
+ {
+ if (mu_begin_treenode(&mu_ctx, "Sprites"))
+ {
+ mu_ctx._style.indent = 14;
+ for(int i=0; i<4; ++i)
+ {
+ sprite_t *sprite = sprites[i];
+ if (mu_begin_treenode(&mu_ctx, sprite_names[i]))
+ {
+ sprintf(sprite_text_buff, "Size %dx%d", sprite->width, sprite->height);
+ mu_text(&mu_ctx, sprite_text_buff);
+ mu_text(&mu_ctx, tex_format_name(sprite_get_format(sprite)));
+
+ if(mu_button(&mu_ctx, "Preview"))mu_open_popup(&mu_ctx, "Texture");
+
+ if (mu_begin_popup(&mu_ctx, "Texture")) {
+ mu_Container *cont = mu_get_current_container(&mu_ctx);
+ cont->rect.w = sprite->width;
+ cont->rect.h = sprite->height;
+ mu_draw_sprite(&mu_ctx, sprite, cont->rect);
+ mu_end_popup(&mu_ctx);
+ }
+ mu_end_treenode(&mu_ctx);
+ }
+ }
+ mu_end_treenode(&mu_ctx);
+ mu_ctx._style.indent = old_indent;
+ }
+ mu_end_treenode(&mu_ctx);
+ }
+
+ mu_end_window(&mu_ctx);
+ }
+ mu_Container *files_cont = mu_get_container(&mu_ctx, "Files");
+
+ // Fixed, static window
+ if (mu_begin_window_ex(&mu_ctx, "Bar", mu_rect(0, display_get_height() -16, 320, 16), MU_OPT_NOTITLE | MU_OPT_NORESIZE | MU_OPT_NOSCROLL | MU_OPT_NOCLOSE)) {
+ mu_layout_begin_column(&mu_ctx);
+ mu_layout_row(&mu_ctx, 4, (int[]) { 48, 48, 92, 130 }, 0);
+
+ if(!cube_cont->open) {
+ if(mu_button(&mu_ctx, "3D-Cube"))cube_cont->open = true;
+ } else {
+ mu_label(&mu_ctx, "");
+ }
+
+ if(!files_cont->open) {
+ if(mu_button(&mu_ctx, "Files"))files_cont->open = true;
+ } else {
+ mu_label(&mu_ctx, "");
+ }
+
+ mu_label(&mu_ctx, "");
+ mu_label(&mu_ctx, "(Press L to toggle UI)");
+
+ mu_layout_end_column(&mu_ctx);
+ mu_end_window(&mu_ctx);
+ }
+}
+
+void game_draw()
+{
+ if(cube_visible)
+ {
+ glMatrixMode(GL_MODELVIEW);
+ glPushMatrix();
+ glRotatef(box_rot, 0,1,0);
+ glRotatef(box_rot*0.3f, 1,0,0);
+
+ if(capture_cube) {
+ glViewport(
+ cube_win_rect.x,
+ display_get_height() - cube_win_rect.y - cube_win_rect.h,
+ cube_win_rect.w,
+ cube_win_rect.h - 24
+ );
+ } else {
+ glViewport(0,0, display_get_width(), display_get_height());
+ }
+
+ glCallList(dpl_cube);
+ glPopMatrix();
+ }
+}
+
+int main(void)
+{
+ debug_init_emulog();
+ debug_init_usblog();
+
+ joypad_init();
+ dfs_init(DFS_DEFAULT_LOCATION);
+ display_init(RESOLUTION_320x240, DEPTH_16_BPP, 3, GAMMA_NONE, FILTERS_RESAMPLE_ANTIALIAS_DEDITHER);
+ rdpq_init();
+ gl_init();
+
+ #ifdef DEBUG_RDP
+ rdpq_debug_start();
+ #endif
+
+ surface_t zbuffer = surface_alloc(FMT_RGBA16, display_get_width(), display_get_height());
+
+ rainbow(hue, &clear_color[0], &clear_color[1], &clear_color[2]);
+ dpl_cube = create_cube_dpl();
+
+ char sprite_name_buff[32];
+ for(int i=0; iwave.frequency;
while (1) {
display_context_t disp = display_get();
@@ -47,6 +46,9 @@ int main(void) {
graphics_draw_text(disp, 50, 70, "B - Play laser (keep pressed)");
graphics_draw_text(disp, 50, 80, "Z - Start / stop background music");
graphics_draw_text(disp, 70, 90, "L/R - Change music frequency");
+ graphics_draw_text(disp, 50, 100, "C-Left / C-Right - Cannon panned hard L/R");
+ graphics_draw_text(disp, 50, 110, "C-Up - Toggle force mono");
+ graphics_draw_text(disp, 50, 120, force_mono ? "Output: MONO" : "Output: STEREO");
graphics_draw_text(disp, 50, 140, "Music courtesy of MishtaLu / indiegamemusic.com");
display_show(disp);
@@ -54,17 +56,39 @@ int main(void) {
joypad_buttons_t ckeys = joypad_get_buttons_pressed(JOYPAD_PORT_1);
if (ckeys.a) {
- wav64_play(&sfx_cannon, CHANNEL_SFX1);
+ wav64_play(sfx_cannon, CHANNEL_SFX1);
+ // Restore centered vol — C-Left/C-Right leave SFX1's
+ // stored vol hard-panned, and mixer_ch_play does not
+ // reset it.
+ mixer_ch_set_vol(CHANNEL_SFX1, 1.0f, 1.0f);
}
if (ckeys.b) {
- wav64_play(&sfx_laser, CHANNEL_SFX2);
+ wav64_play(sfx_laser, CHANNEL_SFX2);
mixer_ch_set_vol(CHANNEL_SFX2, 0.25f, 0.25f);
}
+ if (ckeys.c_left) {
+ // Hard-pan to the left so the force-mono toggle has an audible
+ // effect: with it off, the cannon only hits the left speaker;
+ // with it on, it splits across both at half amplitude.
+ wav64_play(sfx_cannon, CHANNEL_SFX1);
+ mixer_ch_set_vol(CHANNEL_SFX1, 1.0f, 0.0f);
+ }
+ if (ckeys.c_right) {
+ wav64_play(sfx_cannon, CHANNEL_SFX1);
+ mixer_ch_set_vol(CHANNEL_SFX1, 0.0f, 1.0f);
+ }
+ if (ckeys.c_up) {
+ force_mono = !force_mono;
+ // Global toggle: re-applies the flag across every channel
+ // the mixer currently owns. Voices already playing pick
+ // up the change at the next mixer_poll without restart.
+ mixer_set_force_mono(force_mono);
+ }
if (ckeys.z) {
music = !music;
if (music) {
- wav64_play(&sfx_monosample, CHANNEL_MUSIC);
- music_frequency = sfx_monosample.wave.frequency;
+ wav64_play(sfx_monosample, CHANNEL_MUSIC);
+ music_frequency = sfx_monosample->wave.frequency;
}
else
mixer_ch_stop(CHANNEL_MUSIC);
@@ -86,10 +110,6 @@ int main(void) {
// Check whether one audio buffer is ready, otherwise wait for next
// frame to perform mixing.
- if (audio_can_write()) {
- short *buf = audio_write_begin();
- mixer_poll(buf, audio_get_buffer_length());
- audio_write_end();
- }
+ mixer_try_play();
}
}
diff --git a/examples/overlays/actor/Makefile b/examples/overlays/actor/Makefile
index 07835c06f..e347d3d6b 100644
--- a/examples/overlays/actor/Makefile
+++ b/examples/overlays/actor/Makefile
@@ -31,7 +31,7 @@ filesystem/n64brew.dso: $(n64brew_SRC:%.c=$(BUILD_DIR)/%.o)
overlays_actor.z64: N64_ROM_TITLE="Actor Overlay Demo"
overlays_actor.z64: $(BUILD_DIR)/overlays_actor.dfs $(BUILD_DIR)/overlays_actor.msym
-$(BUILD_DIR)/overlays_actor.msym: $(BUILD_DIR)/overlays_actor.elf
+$(BUILD_DIR)/overlays_actor.msym: $(BUILD_DIR)/overlays_actor.elf $(MAIN_ELF_EXTERNS)
clean:
rm -rf $(BUILD_DIR) filesystem $(DSO_LIST) overlays_actor.z64
diff --git a/examples/overlays/actor/overlays_actor.c b/examples/overlays/actor/overlays_actor.c
index 08b30978e..ffe21471b 100644
--- a/examples/overlays/actor/overlays_actor.c
+++ b/examples/overlays/actor/overlays_actor.c
@@ -100,7 +100,7 @@ int main()
debug_init_emulog();
debug_init_usblog();
//Init rendering
- display_init(RESOLUTION_320x240, DEPTH_16_BPP, 3, GAMMA_NONE, FILTERS_RESAMPLE);
+ display_init(RESOLUTION_320x240, DEPTH_16_BPP, 3, GAMMA_NONE, FILTERS_DISABLED);
rdpq_init();
rdpq_debug_start();
scr_width = display_get_width();
diff --git a/examples/overlays/scene/Makefile b/examples/overlays/scene/Makefile
index ad015f0a1..c439aef2f 100644
--- a/examples/overlays/scene/Makefile
+++ b/examples/overlays/scene/Makefile
@@ -30,7 +30,7 @@ filesystem/scene_common.dso: $(scene_common_SRC:%.cpp=$(BUILD_DIR)/%.o)
filesystem/scene/bg_test.dso: $(bgtest_SRC:%.cpp=$(BUILD_DIR)/%.o)
filesystem/scene/sprite_test.dso: $(spritetest_SRC:%.cpp=$(BUILD_DIR)/%.o)
-$(BUILD_DIR)/overlays_scene.msym: $(BUILD_DIR)/overlays_scene.elf
+$(BUILD_DIR)/overlays_scene.msym: $(BUILD_DIR)/overlays_scene.elf $(MAIN_ELF_EXTERNS)
overlays_scene.z64: N64_ROM_TITLE="Actor Overlay Demo"
overlays_scene.z64: $(BUILD_DIR)/overlays_scene.dfs $(BUILD_DIR)/overlays_scene.msym
diff --git a/examples/overlays/scene/overlays_scene.cpp b/examples/overlays/scene/overlays_scene.cpp
index a6d723e1d..0b86bab49 100644
--- a/examples/overlays/scene/overlays_scene.cpp
+++ b/examples/overlays/scene/overlays_scene.cpp
@@ -7,7 +7,7 @@ int main()
debug_init_emulog();
debug_init_usblog();
//Init rendering
- display_init(RESOLUTION_320x240, DEPTH_16_BPP, 3, GAMMA_NONE, FILTERS_RESAMPLE);
+ display_init(RESOLUTION_320x240, DEPTH_16_BPP, 3, GAMMA_NONE, FILTERS_DISABLED);
rdpq_init();
rdpq_debug_start();
//Init miscellaneous system
diff --git a/examples/ovldemo/.gitignore b/examples/ovldemo/.gitignore
new file mode 100644
index 000000000..ecc834d58
--- /dev/null
+++ b/examples/ovldemo/.gitignore
@@ -0,0 +1 @@
+filesystem/
diff --git a/examples/ovldemo/Makefile b/examples/ovldemo/Makefile
new file mode 100644
index 000000000..8c35cbe45
--- /dev/null
+++ b/examples/ovldemo/Makefile
@@ -0,0 +1,48 @@
+BUILD_DIR=build
+include $(N64_INST)/include/n64.mk
+
+# Main Executable Source File Lists
+main_SRC = ovldemo.c
+
+# Specify Source Files per Overlay
+overlay1_SRC = overlay1.c
+overlay2_SRC = overlay2.c
+
+#Main ROM Build Rule
+all: ovldemo.z64
+
+# File for DSO External Symbols List
+# This file is used to make sure symbols used only in DSOs
+# exist in the main executable
+MAIN_ELF_EXTERNS := $(BUILD_DIR)/ovldemo.externs
+# File for Main Executable Symbol Table
+MAIN_ELF_SYMBOLS := $(BUILD_DIR)/ovldemo.msym
+
+# Overlay File List
+DSO_MODULES = overlay1.dso overlay2.dso
+DSO_LIST = $(addprefix filesystem/, $(DSO_MODULES))
+# Filesystem Build Rule
+$(BUILD_DIR)/ovldemo.dfs: $(DSO_LIST)
+# Main ELF Build Rule
+# DSO External Symbols List must be specified here
+$(BUILD_DIR)/ovldemo.elf: $(main_SRC:%.c=$(BUILD_DIR)/%.o) $(MAIN_ELF_EXTERNS)
+# Build DSO External Symbol List
+$(MAIN_ELF_EXTERNS): $(DSO_LIST)
+
+# Specify object files (.o) that are part of each overlay
+filesystem/overlay1.dso: $(overlay1_SRC:%.c=$(BUILD_DIR)/%.o)
+filesystem/overlay2.dso: $(overlay2_SRC:%.c=$(BUILD_DIR)/%.o)
+
+ovldemo.z64: N64_ROM_TITLE="Overlay Demo"
+# ROM Build rules
+# Main Executable Symbol Table is required for overlay linking
+ovldemo.z64: $(BUILD_DIR)/ovldemo.dfs $(MAIN_ELF_SYMBOLS)
+# Rule for Main Executable Symbol Table
+$(MAIN_ELF_SYMBOLS): $(BUILD_DIR)/ovldemo.elf $(MAIN_ELF_EXTERNS)
+
+clean:
+ rm -rf $(BUILD_DIR) filesystem $(DSO_LIST) ovldemo.z64
+
+-include $(wildcard $(BUILD_DIR)/*.d)
+
+.PHONY: all clean
diff --git a/examples/ovldemo/overlay1.c b/examples/ovldemo/overlay1.c
new file mode 100644
index 000000000..36ad26dc8
--- /dev/null
+++ b/examples/ovldemo/overlay1.c
@@ -0,0 +1,14 @@
+#include
+#include
+
+void overlay_print(int time)
+{
+ //Calculate time in seconds from passed in ticks
+ float x = time/display_get_fps();
+ printf("Overlay 1 Printer\n");
+ //Do some non-constant math with log1p and cos
+ //log1p is an uncommon math function
+ //This verifies that dead stripping works properly
+ //even if functons are only referenced from an overlay
+ printf("log1p(%f)*cos(%f) = %f\n", x, x, log1p(x)*cos(x));
+}
\ No newline at end of file
diff --git a/examples/ovldemo/overlay2.c b/examples/ovldemo/overlay2.c
new file mode 100644
index 000000000..795dfa4bb
--- /dev/null
+++ b/examples/ovldemo/overlay2.c
@@ -0,0 +1,11 @@
+#include
+#include
+
+void overlay_print(int time)
+{
+ //Calculate time in seconds
+ float x = time/display_get_fps();
+ printf("Overlay 2 Printer\n");
+ //Do some non-constant math to test importing functions
+ printf("sin(%f)=%f\n", x, sin(x));
+}
\ No newline at end of file
diff --git a/examples/ovldemo/ovldemo.c b/examples/ovldemo/ovldemo.c
new file mode 100644
index 000000000..5679ef801
--- /dev/null
+++ b/examples/ovldemo/ovldemo.c
@@ -0,0 +1,61 @@
+#include
+
+//Typedef for symbol
+typedef void (*print_func)(int time);
+
+int main(void)
+{
+ //Initialize debugging output
+ debug_init_emulog();
+ debug_init_usblog();
+ //Initialize joypad/controller
+ joypad_init();
+ //Initialize DFS
+ dfs_init(DFS_DEFAULT_LOCATION);
+
+ //Init timer
+ int time = 0;
+
+ //Initialize console
+ console_init();
+ console_set_render_mode(RENDER_MANUAL);
+
+ //Start with overlay 1 loaded
+ void *ovl_handle = dlopen("rom:/overlay1.dso", RTLD_LOCAL); //Load overlay 1
+ print_func ovl_func = dlsym(ovl_handle, "overlay_print"); //Find overlay_print symbol
+ while(1)
+ {
+ console_clear();
+ //Print header
+ printf("Overlay Demo\n");
+ printf("ovl_handle=%p, ovl_func=%p\n", ovl_handle, ovl_func);
+ printf("time=%d\n", time);
+ printf("\n");
+ //Print overlay specific data
+ ovl_func(time);
+ //Print footer
+ printf("\nControls:\n");
+ printf("A: Load Overlay 1\n");
+ printf("B: Load Overlay 2\n");
+ console_render();
+ //Handle buttons
+ joypad_poll();
+ joypad_buttons_t keys = joypad_get_buttons_pressed(JOYPAD_PORT_1);
+ if(keys.a) {
+ //Switch to overlay 1
+ dlclose(ovl_handle); //Close previous loaded overlay
+ ovl_handle = dlopen("rom:/overlay1.dso", RTLD_LOCAL); //Load overlay 1
+ ovl_func = dlsym(ovl_handle, "overlay_print"); //Find overlay_print symbol
+ time = 0; //Reset timer
+ }
+ if(keys.b) {
+ //Switch to overlay 2
+ dlclose(ovl_handle); //Close previous loaded overlay
+ ovl_handle = dlopen("rom:/overlay2.dso", RTLD_LOCAL); //Load overlay 2
+ ovl_func = dlsym(ovl_handle, "overlay_print"); //Find overlay_print symbol
+ time = 0; //Reset timer
+ }
+ //Increment timer
+ time = (time+1)%1000;
+ }
+}
diff --git a/examples/pixelshader/.gitignore b/examples/pixelshader/.gitignore
new file mode 100644
index 000000000..b2552045c
--- /dev/null
+++ b/examples/pixelshader/.gitignore
@@ -0,0 +1,3 @@
+filesystem/
+build/
+*.z64
diff --git a/examples/pixelshader/Makefile b/examples/pixelshader/Makefile
new file mode 100644
index 000000000..bbf53b4a0
--- /dev/null
+++ b/examples/pixelshader/Makefile
@@ -0,0 +1,40 @@
+BUILD_DIR=build
+include $(N64_INST)/include/n64.mk
+
+src = pixelshader.c
+asm = rsp_blend.S
+assets_ttf = $(wildcard assets/*.ttf)
+assets_png = $(wildcard assets/*.png)
+
+assets_conv = $(addprefix filesystem/,$(notdir $(assets_ttf:%.ttf=%.font64))) \
+ $(addprefix filesystem/,$(notdir $(assets_png:%.png=%.sprite)))
+
+MKSPRITE_FLAGS ?= --compress
+MKFONT_FLAGS ?= --compress --size 20 --range 20-7F
+
+all: pixelshader.z64
+
+filesystem/%.font64: assets/%.ttf
+ @mkdir -p $(dir $@)
+ @echo " [FONT] $@"
+ $(N64_MKFONT) $(MKFONT_FLAGS) -o filesystem "$<"
+
+filesystem/%.sprite: assets/%.png
+ @mkdir -p $(dir $@)
+ @echo " [SPRITE] $@"
+ $(N64_MKSPRITE) $(MKSPRITE_FLAGS) -o filesystem "$<"
+
+filesystem/ComicSans.font64: MKFONT_FLAGS += -v
+
+$(BUILD_DIR)/pixelshader.dfs: $(assets_conv)
+$(BUILD_DIR)/pixelshader.elf: $(src:%.c=$(BUILD_DIR)/%.o) $(asm:%.S=$(BUILD_DIR)/%.o)
+
+pixelshader.z64: N64_ROM_TITLE="Pixel Shader Demo"
+pixelshader.z64: $(BUILD_DIR)/pixelshader.dfs
+
+clean:
+ rm -rf $(BUILD_DIR) filesystem pixelshader.z64
+
+-include $(wildcard $(BUILD_DIR)/*.d)
+
+.PHONY: all clean
diff --git a/examples/pixelshader/assets/background.png b/examples/pixelshader/assets/background.png
new file mode 100644
index 000000000..27ed6c6f6
Binary files /dev/null and b/examples/pixelshader/assets/background.png differ
diff --git a/examples/pixelshader/assets/encode.ttf b/examples/pixelshader/assets/encode.ttf
new file mode 100644
index 000000000..faa44c81c
Binary files /dev/null and b/examples/pixelshader/assets/encode.ttf differ
diff --git a/examples/pixelshader/assets/flare1.png b/examples/pixelshader/assets/flare1.png
new file mode 100644
index 000000000..bb8f8ea13
Binary files /dev/null and b/examples/pixelshader/assets/flare1.png differ
diff --git a/examples/pixelshader/pixelshader.c b/examples/pixelshader/pixelshader.c
new file mode 100644
index 000000000..5a444fb25
--- /dev/null
+++ b/examples/pixelshader/pixelshader.c
@@ -0,0 +1,141 @@
+/**
+ * Pixelshader - example of using RSP to manipulate pixels
+ *
+ * This example shows how to achieve additive alpha blending using RSP.
+ * It is meant just as an example of doing per-pixel effects with RSP. The
+ * name "pixel shader" is catchy but the technique cannot be used as a real
+ * pixel shader: it is only possible to preprocess a texture/sprite using
+ * RSP before using RDP to draw it.
+ */
+
+#include
+#include "rsp_blend_constants.h"
+
+static uint32_t ovl_id;
+static void rsp_blend_assert_handler(rsp_snapshot_t *state, uint16_t code);
+
+enum {
+ // Overlay commands. This must match the command table in the RSP code
+ RSP_BLEND_CMD_SET_SOURCE = 0x0,
+ RSP_BLEND_CMD_PROCESS_LINE = 0x1,
+};
+
+// Overlay definition
+DEFINE_RSP_UCODE(rsp_blend,
+ .assert_handler = rsp_blend_assert_handler);
+
+void rsp_blend_init(void) {
+ // Initialize if rspq (if it isn't already). It's best practice to let all overlays
+ // always call rspq_init(), so that they can be themselves initialized in any order
+ // by the user.
+ rspq_init();
+ ovl_id = rspq_overlay_register(&rsp_blend);
+}
+
+void rsp_blend_assert_handler(rsp_snapshot_t *state, uint16_t code) {
+ switch (code) {
+ case ASSERT_INVALID_WIDTH:
+ printf("Invalid surface width (%ld)\nMust be multiple of 8 and less than 640\n",
+ state->gpr[8]); // read current width from t0 (reg #8): we know it's there at assert point
+ return;
+ }
+}
+
+void rsp_blend_set_source(surface_t *src) {
+ assertf(surface_get_format(src) == FMT_RGBA16,
+ "rsp_blend only handles RGB555 surfaces");
+ rspq_write(ovl_id, RSP_BLEND_CMD_SET_SOURCE, PhysicalAddr(src->buffer),
+ (src->width << 16) | src->height);
+}
+
+void rsp_blend_process_line(surface_t *dest, int x0, int y0, int numlines) {
+ assertf(surface_get_format(dest) == FMT_RGBA16,
+ "rsp_blend only handles RGB555 surfaces");
+
+ void *line = dest->buffer + y0 * dest->stride + x0 * 2;
+ for (int i=0; istride;
+ }
+}
+
+int main(void) {
+ debug_init_emulog();
+ debug_init_usblog();
+ display_init(RESOLUTION_640x480, DEPTH_16_BPP, 2, GAMMA_NONE, FILTERS_DISABLED);
+ dfs_init(DFS_DEFAULT_LOCATION);
+ joypad_init();
+ rdpq_init();
+ rdpq_debug_start();
+
+ sprite_t* bkg = sprite_load("rom:/background.sprite");
+ sprite_t* flare1 = sprite_load("rom:/flare1.sprite");
+ rdpq_font_t *font = rdpq_font_load("rom:/encode.font64");
+ enum { MYFONT = 1 };
+ rdpq_text_register_font(MYFONT, font);
+
+ surface_t bkgsurf = sprite_get_pixels(bkg);
+ surface_t flrsurf = sprite_get_pixels(flare1);
+
+ rsp_blend_init(); // init our custom overlay
+
+ bool use_rdp = false;
+
+ uint32_t last_frame = 0;
+ uint32_t cur_frame = 0;
+
+ while (1) {
+ cur_frame = TICKS_READ();
+
+ surface_t *screen = display_get();
+ rdpq_attach(screen, NULL);
+
+ // Draw help text on the top of the screen
+ rdpq_set_mode_fill(RGBA32(0,0,0,0));
+ rdpq_fill_rectangle(0, 0, screen->width, 30);
+ rdpq_text_printf(NULL, MYFONT, 40, 20, "Additive blending with %s (press A to toggle) -- %d us", use_rdp ? "RDP" : "RSP", TIMER_MICROS(last_frame));
+
+ // Draw the background
+ rdpq_set_mode_copy(true);
+ rdpq_tex_blit(&bkgsurf, 0, 30, NULL);
+
+ if (use_rdp) {
+ // Draw the flare using RDP additive blending (will overflow)
+ rdpq_set_mode_standard();
+ rdpq_mode_blender(RDPQ_BLENDER_ADDITIVE);
+ rdpq_tex_blit(&flrsurf, 30, 60, NULL);
+ rdpq_detach_show();
+ } else {
+ // Detach the RDP.
+ rdpq_detach();
+
+ // Add a fence. This makes the RSP wait until the RDP has finished drawing,
+ // which is what we need as we are going to process the pixels of the background
+ // with the RSP.
+ rdpq_fence();
+
+ // Configure source surface
+ rsp_blend_set_source(&flrsurf);
+
+ // Apply blending
+ rsp_blend_process_line(screen, 30, 60, flrsurf.height);
+
+ // Wait for RSP to finish processing
+ rspq_wait();
+
+ // Draw the flare using RSP additive blending (will not overflow)
+ display_show(screen);
+ }
+
+ // Wait until RSP+RDP are idle. This is normally not required, but we force it here
+ // to measure the exact frame computation time.
+ rspq_wait();
+ last_frame = TICKS_READ() - cur_frame;
+
+ joypad_poll();
+ joypad_buttons_t keys = joypad_get_buttons_pressed(JOYPAD_PORT_1);
+ if (keys.a) {
+ use_rdp = !use_rdp;
+ }
+ }
+}
diff --git a/examples/pixelshader/rsp_blend.S b/examples/pixelshader/rsp_blend.S
new file mode 100644
index 000000000..c95bf58ad
--- /dev/null
+++ b/examples/pixelshader/rsp_blend.S
@@ -0,0 +1,226 @@
+###################################################################
+#
+# Example RSPQ overlay that does pixel processing on the RSP.
+#
+###################################################################
+
+# All rspq overlays must include rspq_queue.inc
+#include
+
+# This is an internal header to share some constants between the C code and the assembly.
+#include "rsp_blend_constants.h"
+
+ .set noreorder
+ .set at
+
+ .data
+
+# Standard RSPQ command table. This defines the entrypoints that can be called from C.
+# In this case, we define two commands. The number "8" refers to the number of bytes the
+# commands is made of. Currently, rspq only allow commands to be specified in multipe of
+# 32-bit words, so this nunber is always expected to be a multiple of 4.
+#
+# The commands are:
+# * BlendCmd_SetSource: this is used to configure the rspq overlay with the pointer to
+# source texture to blend (the lens flare)
+# * BlendCmd_Add: this is used to blend the previously_configured source texture into
+# the destination texture.
+#
+# Notice that this is just an example; we could have used just one commmand receiving
+# both source and destination pointers, or three commands (source, dest, process). There
+# would be little differences anyway.
+#
+RSPQ_BeginOverlayHeader
+ RSPQ_DefineCommand BlendCmd_SetSource, 8 # 0x0
+ RSPQ_DefineCommand BlendCmd_ProcessLine, 4 # 0x1
+RSPQ_EndOverlayHeader
+
+# Standard RSPQ DMEM state. This block must contain all variables that must save
+# their value *between* calls to the overlay, that is between different commands.
+# In general, anytime the overlay is swapped out and then back in, the data segment
+# is restored ot its initial value. The only exception is this saved state, which is
+# saved into RDRAM and then restored in DMEM anytime the overlay is swapped.
+RSPQ_BeginSavedState
+SRC_RDRAM: .long 0 # Pointer to the source image in RDRAM
+SRC_WIDTH: .half 0 # Source image width in bytes
+SRC_HEIGHT: .half 0 # Source image height
+RSPQ_EndSavedState
+
+ .balign 16
+VCONST: .half 0x1F << 10 # 5-bit mask in bits 14..10
+ .half 0 # Padding
+ .half 0
+ .half 0
+ .half 0
+ .half 0
+ .half 0
+ .half 0
+
+ # The BSS segment holds all uniniitialized memory buffers
+ # Notice that the contents of this variable is *random* (not zero).
+ # RSP code should never expect these to be zero-initialized.
+ .bss
+
+ .balign 8 # Buffers accessed via DMA must be 8-byte aligned
+SRC_BUF: .dcb.b 640*2 # Source image buffer
+DST_BUF: .dcb.b 640*2+8 # Dest image buffer (+ 8 bytes to handle misalignment of RDRAM pointer)
+
+ .text
+
+ #######################################
+ # BlendCmd_SetSource
+ #
+ # Set source image pointer and size
+ #
+ # Input:
+ # a0 = pointer to source image in RDRAM
+ # a1 = source image width (top 16 bits) + height (bottom 16 bits)
+ #
+ #######################################
+ .func BlendCmd_SetSource
+BlendCmd_SetSource:
+ # Extract width and check if it's a multiple of 8
+ srl t0, a1, 16
+ andi t1, t0, 0x7
+ assert_eq t1, 0, ASSERT_INVALID_WIDTH
+ li t1, 640
+ assert_le t0, t1, ASSERT_INVALID_WIDTH
+
+ # Multiply width by 2 (16-bit pixels)
+ sll t0, 1
+
+ sh t0, %lo(SRC_WIDTH) # Save source width
+ sh a1, %lo(SRC_HEIGHT) # Save source height
+ jr ra
+ sw a0, %lo(SRC_RDRAM) # Save source pointer
+ .endfunc
+
+
+ .func BlendCmd_ProcessLine
+
+ # Register allocation: define aliases for readability
+ #define width t7
+ #define src_ptr s3
+ #define dst_ptr s4
+
+ #define vsrc16 $v01
+ #define vsrc_r $v02
+ #define vsrc_g $v03
+ #define vsrc_b $v04
+ #define vdst16 $v05
+ #define vdst_r $v06
+ #define vdst_g $v07
+ #define vdst_b $v08
+
+ #define vconst $v29
+ #define k_color_mask vconst.e0
+
+BlendCmd_ProcessLine:
+
+ # Fetch source buffer into DMEM. Notice that we use to async version
+ # of DMAIn here as we don't need to wait for the DMA to complete, we
+ # can just continue.
+ lw s0, %lo(SRC_RDRAM)
+ li s4, %lo(SRC_BUF)
+ lh t0, %lo(SRC_WIDTH)
+ jal DMAInAsync
+ addi t0, -1
+
+ # Fetch destination buffer into DMEM. This is a sync version that will
+ # wait for this (and the previous!) DMA to complete.
+ # NOTE: the RDRAM pointer might be misaligned (RSP DMA requires 8-byte alignment).
+ # DMAIn will automatically adjust the pointer to the previous 8-byte boundary, but
+ # we need to fetch 8 bytes more to make sure the correct pixels are fetched.
+ move s0, a0
+ li s4, %lo(DST_BUF)
+ lh t0, %lo(SRC_WIDTH)
+ jal DMAIn
+ addi t0, +8-1
+
+ # Iniitalize src_ptr and width.
+ # Notice that after the previous DMAIn, dst_ptr (s4) already points
+ # to the correct first pixel in DMEM (as DMAIn adjusts it to match the
+ # RDRAM misalignment). See DMAIn documentation for details.
+ li src_ptr, %lo(SRC_BUF)
+ lh width, %lo(SRC_WIDTH)
+
+ li s0, %lo(VCONST) # Read constants used below
+ lqv vconst, 0,s0
+
+line_loop:
+ # Fetch 8 16-bit pixels from source and dest buffer. dest buffer in DMEM might be
+ # misaligned, so we need the lqv/lrv sequence to fully load all the pixels
+ lqv vsrc16, 0,src_ptr
+ lqv vdst16, 0,dst_ptr
+ lrv vdst16, 16,dst_ptr
+
+ # Isolate R,G,B components. Since we want to handle saturation during addition,
+ # we need to scale each component into a 16-bit signed integer, which means
+ # that the 5-bit components must go into bits 14..10. Thus:
+ #
+ # R is at 15..11, so it must be shift right by 1
+ # G is at 10..6, so it must be shift left by 4
+ # B is at 5..1, so it must be shift left by 9
+ vsrl vsrc_r, vsrc16, 1
+ vsll vsrc_g, vsrc16, 4
+ vsll8 vsrc_b, vsrc16, 9
+
+ # Keep only correct bits
+ vand vsrc_r, k_color_mask
+ vand vsrc_g, k_color_mask
+ vand vsrc_b, k_color_mask
+
+ # Now do the same for destination pixels
+ vsrl vdst_r, vdst16, 1
+ vsll vdst_g, vdst16, 4
+ vsll8 vdst_b, vdst16, 9
+ vand vdst_r, k_color_mask
+ vand vdst_g, k_color_mask
+ vand vdst_b, k_color_mask
+
+ # Add source and destination pixels. VADD performs saturation automatically.
+ vadd vdst_r, vsrc_r
+ vadd vdst_g, vsrc_g
+ vadd vdst_b, vsrc_b
+
+ # Now we need to pack the result back into 16-bit pixels. We reverse the shifts
+ vsll vdst_r, vdst_r, 1
+ vsrl vdst_g, vdst_g, 4
+ vsrl8 vdst_b, vdst_b, 9
+ vor vdst16, vdst_r, vdst_g
+ vor vdst16, vdst_b
+
+ # Store result (again, could be misaligned)
+ sqv vdst16, 0,dst_ptr
+ srv vdst16, 16,dst_ptr
+
+ # Increment pointers
+ addi src_ptr, 8*2
+ addi dst_ptr, 8*2
+
+ # 8 pixels done, check if we're done
+ addi width, -8
+ bgez width, line_loop
+ nop
+
+ # Now DMA back into RDRAM
+ move s0, a0
+ li s4, %lo(DST_BUF)
+ lh t0, %lo(SRC_WIDTH)
+ jal DMAOut
+ addi t0, +8-1
+
+ # Increment line pointer of the source image. Next time we're called,
+ # we'll fetch the next line.
+ lw t0, %lo(SRC_RDRAM)
+ lh t1, %lo(SRC_WIDTH)
+ add t0, t1
+ sw t0, %lo(SRC_RDRAM)
+
+ # Done! Go back to main loop
+ # NOTE: we can't do "jr ra" here as "ra" was discarded by the previous DMA function calls.
+ # Jumping to RSPQ_Loop is the standard way to finish an overlay command, in these cases.
+ j RSPQ_Loop
+ nop
+
+ .endfunc
diff --git a/examples/pixelshader/rsp_blend_constants.h b/examples/pixelshader/rsp_blend_constants.h
new file mode 100644
index 000000000..6dda1a6bf
--- /dev/null
+++ b/examples/pixelshader/rsp_blend_constants.h
@@ -0,0 +1,6 @@
+#ifndef RSP_BLEND_CONSTANTS_H
+#define RSP_BLEND_CONSTANTS_H
+
+#define ASSERT_INVALID_WIDTH 0x0101
+
+#endif
diff --git a/examples/rdpqdemo/Makefile b/examples/rdpqdemo/Makefile
index 6f8533042..1f1ffedfe 100644
--- a/examples/rdpqdemo/Makefile
+++ b/examples/rdpqdemo/Makefile
@@ -33,7 +33,7 @@ rdpqdemo.z64: N64_ROM_TITLE="RSPQ Demo"
rdpqdemo.z64: $(BUILD_DIR)/rdpqdemo.dfs
clean:
- rm -rf $(BUILD_DIR) rdpqdemo.z64
+ rm -rf $(BUILD_DIR) rdpqdemo.z64 filesystem
-include $(wildcard $(BUILD_DIR)/*.d)
diff --git a/examples/rdpqdemo/assets/Caverns16bit.xm b/examples/rdpqdemo/assets/Caverns16bit.xm
new file mode 100644
index 000000000..b3dba7926
Binary files /dev/null and b/examples/rdpqdemo/assets/Caverns16bit.xm differ
diff --git a/examples/rdpqdemo/assets/cannon.wav b/examples/rdpqdemo/assets/cannon.wav
new file mode 100644
index 000000000..61928e712
Binary files /dev/null and b/examples/rdpqdemo/assets/cannon.wav differ
diff --git a/examples/rspqdemo/assets/Caverns16bit.xm b/examples/rspqdemo/assets/Caverns16bit.xm
new file mode 100644
index 000000000..b3dba7926
Binary files /dev/null and b/examples/rspqdemo/assets/Caverns16bit.xm differ
diff --git a/examples/rspqdemo/assets/cannon.wav b/examples/rspqdemo/assets/cannon.wav
new file mode 100644
index 000000000..61928e712
Binary files /dev/null and b/examples/rspqdemo/assets/cannon.wav differ
diff --git a/examples/rspqdemo/assets/n64brew.png b/examples/rspqdemo/assets/n64brew.png
new file mode 100644
index 000000000..106eb6b4a
Binary files /dev/null and b/examples/rspqdemo/assets/n64brew.png differ
diff --git a/examples/rspqdemo/assets/tiles.png b/examples/rspqdemo/assets/tiles.png
new file mode 100644
index 000000000..c923ada12
Binary files /dev/null and b/examples/rspqdemo/assets/tiles.png differ
diff --git a/examples/rtctest/Makefile b/examples/rtctest/Makefile
index ed2452c30..acd7edb43 100644
--- a/examples/rtctest/Makefile
+++ b/examples/rtctest/Makefile
@@ -2,11 +2,14 @@ all: rtctest.z64
.PHONY: all
BUILD_DIR = build
+# This example uses some rtc preview APIs
+LIBDRAGON_PREVIEW = 2
include $(N64_INST)/include/n64.mk
OBJS = $(BUILD_DIR)/rtctest.o
rtctest.z64: N64_ROM_TITLE = "RTC Test"
+rtctest.z64: N64_ROM_CATEGORY = C
rtctest.z64: N64_ROM_RTC = true
$(BUILD_DIR)/rtctest.elf: $(OBJS)
diff --git a/examples/rtctest/constants.h b/examples/rtctest/constants.h
new file mode 100644
index 000000000..501714863
--- /dev/null
+++ b/examples/rtctest/constants.h
@@ -0,0 +1,66 @@
+#ifndef RTCTEST_CONSTANTS_H
+#define RTCTEST_CONSTANTS_H
+
+#define BLACK 0x000000FF
+#define WHITE 0xFFFFFFFF
+
+#define JOYSTICK_DEAD_ZONE 32
+
+#define MAX(a,b) ({ typeof(a) _a = a; typeof(b) _b = b; _a > _b ? _a : _b; })
+#define MIN(a,b) ({ typeof(a) _a = a; typeof(b) _b = b; _a < _b ? _a : _b; })
+#define CLAMP(x, min, max) (MIN(MAX((x), (min)), (max)))
+#define WRAP(x, min, max) ({ \
+ typeof(x) _x = x; typeof(min) _min = min; typeof(max) _max = max; \
+ _x < _min ? _max : _x > _max ? _min : _x; \
+})
+
+#define SECONDS_PER_YEAR (60.0f * 60.0f * 24.0f * 365.2422f)
+#define TIMESTAMP_TO_YEAR(ts) CLAMP((int)(roundf((ts) / SECONDS_PER_YEAR)) + 1970, 1900, 2199)
+
+#define GLYPH_WIDTH 8
+#define GLYPH_HEIGHT 8
+
+#define LINE1 (8 * GLYPH_HEIGHT)
+#define LINE2 (12 * GLYPH_HEIGHT)
+#define LINE3 (14 * GLYPH_HEIGHT)
+#define LINE4 (18 * GLYPH_HEIGHT)
+#define LINE5 (20 * GLYPH_HEIGHT)
+#define LINE6 (22 * GLYPH_HEIGHT)
+#define LINE7 (24 * GLYPH_HEIGHT)
+
+/* Line 2 */
+#define YEAR_X (12 * GLYPH_WIDTH)
+#define MONTH_X (YEAR_X + (5 * GLYPH_WIDTH))
+#define DAY_X (MONTH_X + (3 * GLYPH_WIDTH))
+#define DOW_X (DAY_X + (4 * GLYPH_WIDTH))
+/* Line 3 */
+#define HOUR_X (16 * GLYPH_WIDTH)
+#define MIN_X (HOUR_X + (3 * GLYPH_WIDTH))
+#define SEC_X (MIN_X + (3 * GLYPH_WIDTH))
+
+#define EDIT_YEAR 0x0020
+#define EDIT_MONTH 0x0010
+#define EDIT_DAY 0x0008
+#define EDIT_HOUR 0x0004
+#define EDIT_MIN 0x0002
+#define EDIT_SEC 0x0001
+#define EDIT_NONE 0x0000
+
+
+/* SCREEN_WIDTH_GUIDE: "----------------------------------------" */
+static const char* RUN_SOFT_MESSAGE = " RTC source detected: None ";
+static const char* RUN_JOY_MESSAGE = " RTC source detected: Joybus ";
+static const char* RUN_DD_MESSAGE = " RTC source detected: 64DD ";
+static const char* RUN_BB_MESSAGE = " RTC source detected: iQue ";
+static const char* PAUSED_MESSAGE = " Adjust the current date/time: ";
+static const char* RTC_DATE_FORMAT = " YYYY-MM-DD (DoW) ";
+static const char* RTC_TIME_FORMAT = " HH:MM:SS ";
+static const char* ADJUST_MESSAGE = " Press A to adjust date/time ";
+static const char* CONFIRM_MESSAGE = " Press A to write to RTC ";
+static const char* SRC_JOY_MESSAGE = " Press L to switch to Joybus RTC ";
+static const char* SRC_DD_MESSAGE = " Press L to switch to 64DD RTC ";
+
+static const char* const DAYS_OF_WEEK[7] =
+ { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
+
+#endif
diff --git a/examples/rtctest/rtctest.c b/examples/rtctest/rtctest.c
index 8e0346210..51b673506 100644
--- a/examples/rtctest/rtctest.c
+++ b/examples/rtctest/rtctest.c
@@ -1,250 +1,71 @@
#include
+#include
#include
-#include
#include
-#define BLACK 0x000000FF
-#define WHITE 0xFFFFFFFF
-
-#define JOYSTICK_DEAD_ZONE 32
-
-#define GLYPH_WIDTH 8
-#define GLYPH_HEIGHT 8
-
-#define LINE1 (8 * GLYPH_HEIGHT)
-#define LINE2 (12 * GLYPH_HEIGHT)
-#define LINE3 (14 * GLYPH_HEIGHT)
-#define LINE4 (18 * GLYPH_HEIGHT)
-#define LINE5 (20 * GLYPH_HEIGHT)
-
-/* Line 2 */
-#define YEAR_X (12 * GLYPH_WIDTH)
-#define MONTH_X (YEAR_X + (5 * GLYPH_WIDTH))
-#define DAY_X (MONTH_X + (3 * GLYPH_WIDTH))
-#define DOW_X (DAY_X + (4 * GLYPH_WIDTH))
-/* Line 3 */
-#define HOUR_X (16 * GLYPH_WIDTH)
-#define MIN_X (HOUR_X + (3 * GLYPH_WIDTH))
-#define SEC_X (MIN_X + (3 * GLYPH_WIDTH))
-
-#define EDIT_YEAR 0x0020
-#define EDIT_MONTH 0x0010
-#define EDIT_DAY 0x0008
-#define EDIT_HOUR 0x0004
-#define EDIT_MIN 0x0002
-#define EDIT_SEC 0x0001
-#define EDIT_NONE 0x0000
-
-/* SCREEN_WIDTH_GUIDE: "----------------------------------------" */
-static const char * MISSING_MESSAGE = " Real-time clock not detected. ";
-static const char * HELP_1_MESSAGE = " Double-check the settings for ";
-static const char * HELP_2_MESSAGE = " your emulator or flash cart. ";
-static const char * PROBING_MESSAGE = " Probing the real-time clock... ";
-static const char * RUNNING_MESSAGE = " Reading time from the RTC: ";
-static const char * PAUSED_MESSAGE = " Adjust the current date/time: ";
-static const char * WRITING_MESSAGE = " Setting the clock... ";
-static const char * RTC_DATE_FORMAT = " YYYY-MM-DD (DoW) ";
-static const char * RTC_TIME_FORMAT = " HH:MM:SS ";
-static const char * ADJUST_MESSAGE = " Press A to adjust date/time ";
-static const char * CONFIRM_MESSAGE = " Press A to write to RTC ";
-static const char * RETEST_MESSAGE = " Press B to re-run write test ";
-static const char * NOSTATUS_MESSAGE = " RTC status test failed! ";
-static const char * NOWRITE_MESSAGE = " RTC write test failed! ";
-
-static const char * const DAYS_OF_WEEK[7] =
- { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
-
-static char year[sizeof("YYYY")];
-static char month[sizeof("MM")];
-static char day[sizeof("DD")];
-static const char * dow;
-static char hour[sizeof("HH")];
-static char min[sizeof("MM")];
-static char sec[sizeof("SS")];
-
-static const char * line1_text;
-static const char * line4_text;
-
-static const resolution_t res = RESOLUTION_320x240;
-static const bitdepth_t bit = DEPTH_32_BPP;
-static display_context_t disp = 0;
-static struct controller_data keys_pressed;
-static struct controller_data keys_down;
+#include "constants.h"
+
+static surface_t* disp = 0;
+static joypad_inputs_t pad_inputs = {0};
+static joypad_buttons_t pad_pressed = {0};
static int8_t joystick_x_direction = 0;
-static rtc_time_t rtc_time;
-static bool rtc_writable;
+static struct tm rtc_tm = {0};
static uint16_t edit_mode = EDIT_NONE;
-void set_edit_color( uint16_t mask )
-{
- if( edit_mode & mask ) graphics_set_color( BLACK, WHITE );
- else graphics_set_color( WHITE, BLACK );
-}
-
-int wrap( int val, int min, int max )
-{
- if( val < min ) return max;
- if( val > max ) return min;
- return val;
-}
-
-void adjust_rtc_time( rtc_time_t * t, int incr )
-{
- uint8_t expected_day = 0;
- switch( edit_mode )
- {
- case EDIT_YEAR:
- /* TODO Figure out what the max supported year is */
- t->year = wrap( (int)t->year + incr, 1996, 2037 );
- break;
- case EDIT_MONTH:
- t->month = wrap( (int)t->month + incr, 0, 11 );
- break;
- case EDIT_DAY:
- t->day = wrap( (int)t->day + incr, 1, 31 );
- expected_day = t->day;
- break;
- case EDIT_HOUR:
- t->hour = wrap( (int)t->hour + incr, 0, 23 );
- break;
- case EDIT_MIN:
- t->min = wrap( (int)t->min + incr, 0, 59 );
- break;
- case EDIT_SEC:
- t->sec = wrap( (int)t->sec + incr, 0, 59 );
- break;
- }
- rtc_normalize_time( t );
- /* Handle wrap-around for normalized day of month */
- if( expected_day && expected_day != t->day && incr > 0 ) t->day = 1;
-}
-
-void draw_rtc_time( void )
-{
- /* Format RTC date/time as strings */
- sprintf( year, "%04d", rtc_time.year % 10000 );
- sprintf( month, "%02d", (rtc_time.month % 12) + 1 );
- sprintf( day, "%02d", rtc_time.day % 32 );
- dow = DAYS_OF_WEEK[rtc_time.week_day % 7];
- sprintf( hour, "%02d", rtc_time.hour % 24 );
- sprintf( min, "%02d", rtc_time.min % 60 );
- sprintf( sec, "%02d", rtc_time.sec % 60 );
-
- /* Line 2 */
- graphics_draw_text( disp, 0, LINE2, RTC_DATE_FORMAT );
- set_edit_color( EDIT_YEAR );
- graphics_draw_text( disp, YEAR_X, LINE2, year );
- set_edit_color( EDIT_MONTH );
- graphics_draw_text( disp, MONTH_X, LINE2, month );
- set_edit_color( EDIT_DAY );
- graphics_draw_text( disp, DAY_X, LINE2, day );
- graphics_set_color( WHITE, BLACK );
- graphics_draw_text( disp, DOW_X, LINE2, dow );
-
- /* Line 3 */
- graphics_draw_text( disp, 0, LINE3, RTC_TIME_FORMAT );
- set_edit_color( EDIT_HOUR );
- graphics_draw_text( disp, HOUR_X, LINE3, hour );
- set_edit_color( EDIT_MIN );
- graphics_draw_text( disp, MIN_X, LINE3, min );
- set_edit_color( EDIT_SEC );
- graphics_draw_text( disp, SEC_X, LINE3, sec );
-}
-
-void draw_writing_message( void )
-{
- disp = display_get();
-
- graphics_fill_screen( disp, BLACK );
-
- /* Line 1 */
- graphics_set_color( WHITE, BLACK );
- graphics_draw_text( disp, 0, LINE1, WRITING_MESSAGE );
-
- /* Lines 2 & 3 */
- draw_rtc_time();
-
- display_show( disp );
-}
-
-void run_rtc_write_test( void )
-{
- disp = display_get();
-
- graphics_fill_screen( disp, BLACK );
-
- graphics_set_color( WHITE, BLACK );
- graphics_draw_text( disp, 0, LINE1, PROBING_MESSAGE );
-
- display_show(disp);
-
- rtc_writable = rtc_is_writable();
- if( !rtc_writable ) line4_text = NOWRITE_MESSAGE;
-}
-
-void update_joystick_directions( void )
-{
- /* Treat the X direction as a d-pad single button press */
- if( (keys_pressed.c[0].x < -JOYSTICK_DEAD_ZONE) )
- {
- if( joystick_x_direction == 0 ) keys_down.c[0].left = true;
- joystick_x_direction = -1;
- }
- else if ( keys_pressed.c[0].x > +JOYSTICK_DEAD_ZONE )
- {
- if( joystick_x_direction == 0 ) keys_down.c[0].right = true;
- joystick_x_direction = +1;
- }
- else joystick_x_direction = 0;
-
- /* Treat the Y direction as a d-pad button hold */
- if( keys_pressed.c[0].y > +JOYSTICK_DEAD_ZONE )
- {
- keys_pressed.c[0].up = true;
- }
- else if ( keys_pressed.c[0].y < -JOYSTICK_DEAD_ZONE )
- {
- keys_pressed.c[0].down = true;
- }
-}
+static void set_edit_color( uint16_t edit_mode_mask );
+static void adjust_rtc_time( struct tm * t, int incr );
+static void draw_rtc_time( void );
+static void update_joystick_directions( void );
int main(void)
{
- display_init( res, bit, 2, GAMMA_NONE, FILTERS_RESAMPLE );
- controller_init();
- timer_init();
-
- if( !rtc_init() )
- {
- disp = display_get();
-
- graphics_fill_screen( disp, BLACK );
+ debug_init_emulog();
+ debug_init_usblog();
- graphics_set_color( WHITE, BLACK );
-
- graphics_draw_text( disp, 0, LINE1, MISSING_MESSAGE );
- graphics_draw_text( disp, 0, LINE2, HELP_1_MESSAGE );
- graphics_draw_text( disp, 0, LINE3, HELP_2_MESSAGE );
- graphics_draw_text( disp, 0, LINE4, NOSTATUS_MESSAGE );
-
- display_show( disp );
-
- while(1) { /* Deadloop forever */ }
- }
+ display_init( RESOLUTION_320x240, DEPTH_32_BPP, 2, GAMMA_NONE, FILTERS_DISABLED );
+ joypad_init();
+ timer_init();
- /* Determine if the RTC is writable */
- run_rtc_write_test();
+ rtc_init();
- while(1)
+ while(1)
{
- if( !edit_mode ) rtc_get( &rtc_time );
+ if( !edit_mode )
+ {
+ /* Read the current timestamp from RTC subsystem */
+ time_t now = time( NULL );
+ /* Convert the timestamp into date/time struct */
+ rtc_tm = *gmtime( &now );
+ }
disp = display_get();
graphics_fill_screen( disp, BLACK );
/* Line 1 */
- line1_text = edit_mode ? PAUSED_MESSAGE : RUNNING_MESSAGE;
+ const char* line1_text;
+ if( edit_mode )
+ {
+ line1_text = PAUSED_MESSAGE;
+ }
+ else
+ {
+ switch( rtc_get_source() ) {
+ case RTC_SOURCE_JOYBUS:
+ line1_text = RUN_JOY_MESSAGE;
+ break;
+ case RTC_SOURCE_DD:
+ line1_text = RUN_DD_MESSAGE;
+ break;
+ case RTC_SOURCE_BB:
+ line1_text = RUN_BB_MESSAGE;
+ break;
+ case RTC_SOURCE_NONE:
+ default:
+ line1_text = RUN_SOFT_MESSAGE;
+ break;
+ }
+ }
graphics_set_color( WHITE, BLACK );
graphics_draw_text( disp, 0, LINE1, line1_text );
@@ -252,35 +73,56 @@ int main(void)
draw_rtc_time();
/* Line 4 */
- if( rtc_writable )
- {
- line4_text = edit_mode ? CONFIRM_MESSAGE : ADJUST_MESSAGE;
- }
+ const char* line4_text = edit_mode ? CONFIRM_MESSAGE : ADJUST_MESSAGE;
graphics_set_color( WHITE, BLACK );
graphics_draw_text( disp, 0, LINE4, line4_text );
/* Line 5 */
- if( !edit_mode )
+ graphics_set_color( WHITE, BLACK );
+ if( rtc_get_source() == RTC_SOURCE_JOYBUS && rtc_is_source_available( RTC_SOURCE_DD ) )
+ {
+ graphics_draw_text( disp, 0, LINE5, SRC_DD_MESSAGE );
+ }
+ else if( rtc_get_source() == RTC_SOURCE_DD && rtc_is_source_available( RTC_SOURCE_JOYBUS ) )
{
- graphics_set_color( WHITE, BLACK );
- graphics_draw_text( disp, 0, LINE5, RETEST_MESSAGE );
+ graphics_draw_text( disp, 0, LINE5, SRC_JOY_MESSAGE );
+ }
+
+ if( sys_bbplayer() )
+ {
+ // TODO: Remove BBPlayer RTC internal API usage for debugging
+ int bb_rtc_get_state( void *state, uint64_t *raw );
+ int bb_rtc_get_time( time_t *out );
+
+ uint64_t bb_state = 0;
+ time_t bb_time = 0;
+ int bb_result;
+ char line_buf[41];
+
+ bb_result = bb_rtc_get_state( NULL, &bb_state );
+ sprintf( line_buf, " %016llx %21s", bb_state, rtc_error_str( bb_result ) );
+ graphics_draw_text( disp, 0, LINE6, line_buf );
+
+ bb_result = bb_rtc_get_time( &bb_time );
+ sprintf( line_buf, " %016lld %21s", bb_time, rtc_error_str( bb_result ) );
+ graphics_draw_text( disp, 0, LINE7, line_buf );
}
display_show(disp);
-
- controller_scan();
- keys_pressed = get_keys_pressed();
- keys_down = get_keys_down();
+
+ joypad_poll();
+ pad_inputs = joypad_get_inputs(JOYPAD_PORT_1);
+ pad_pressed = joypad_get_buttons_pressed(JOYPAD_PORT_1);
update_joystick_directions();
/* Toggle edit mode */
- if( rtc_writable && keys_down.c[0].A )
+ if( pad_pressed.a )
{
if( edit_mode )
{
edit_mode = EDIT_NONE;
- draw_writing_message();
- rtc_set( &rtc_time );
+ struct timeval now = { .tv_sec = mktime( &rtc_tm ) };
+ settimeofday( &now, NULL );
}
else
{
@@ -288,38 +130,159 @@ int main(void)
}
}
- if( !edit_mode && keys_down.c[0].B )
+ if( !edit_mode && pad_pressed.r )
+ {
+ /* Resync the time from the RTC source */
+ rtc_set_source( rtc_get_source() );
+ }
+
+ if( !edit_mode && pad_pressed.l )
{
- run_rtc_write_test();
+ if( rtc_get_source() == RTC_SOURCE_JOYBUS )
+ {
+ rtc_set_source( RTC_SOURCE_DD );
+ }
+ else if( rtc_get_source() == RTC_SOURCE_DD )
+ {
+ rtc_set_source( RTC_SOURCE_JOYBUS );
+ }
}
if( edit_mode )
{
/* Move between fields */
- if( keys_down.c[0].left )
+ if( pad_pressed.d_left )
{
edit_mode = edit_mode << 1;
if( edit_mode > EDIT_YEAR ) edit_mode = EDIT_SEC;
}
- else if( keys_down.c[0].right )
+ else if( pad_pressed.d_right )
{
edit_mode = edit_mode >> 1;
if( edit_mode < EDIT_SEC ) edit_mode = EDIT_YEAR;
}
/* Adjust date/time */
- if( keys_pressed.c[0].up )
+ if( pad_inputs.btn.d_up )
{
- adjust_rtc_time( &rtc_time, +1 );
+ adjust_rtc_time( &rtc_tm, +1 );
/* Add a delay so you can just hold the direction */
wait_ms( 100 );
}
- else if( keys_pressed.c[0].down )
+ else if( pad_inputs.btn.d_down )
{
- adjust_rtc_time( &rtc_time, -1 );
+ adjust_rtc_time( &rtc_tm, -1 );
/* Add a delay so you can just hold the direction */
wait_ms( 100 );
}
}
}
}
+
+static void set_edit_color( uint16_t edit_mode_mask )
+{
+ if( edit_mode & edit_mode_mask ) graphics_set_color( BLACK, WHITE );
+ else graphics_set_color( WHITE, BLACK );
+}
+
+static void adjust_rtc_time( struct tm * t, int incr )
+{
+ rtc_range_t rtc_range = rtc_get_supported_range();
+ int year_min = TIMESTAMP_TO_YEAR(rtc_range.min);
+ int year_max = TIMESTAMP_TO_YEAR(rtc_range.max + 1) - 1;
+
+ switch( edit_mode )
+ {
+ case EDIT_YEAR:
+ t->tm_year = WRAP( t->tm_year + incr, year_min - 1900, year_max - 1900 );
+ break;
+ case EDIT_MONTH:
+ t->tm_mon = WRAP( t->tm_mon + incr, 0, 11 );
+ break;
+ case EDIT_DAY:
+ t->tm_mday = WRAP( t->tm_mday + incr, 1, 31 );
+ break;
+ case EDIT_HOUR:
+ t->tm_hour = WRAP( t->tm_hour + incr, 0, 23 );
+ break;
+ case EDIT_MIN:
+ t->tm_min = WRAP( t->tm_min + incr, 0, 59 );
+ break;
+ case EDIT_SEC:
+ t->tm_sec = WRAP( t->tm_sec + incr, 0, 59 );
+ break;
+ }
+ // Recalculate day-of-week and day-of-year
+ time_t timestamp = mktime( t );
+ *t = *gmtime( ×tamp );
+}
+
+static void draw_rtc_time( void )
+{
+ rtc_range_t rtc_range = rtc_get_supported_range();
+ int year_min = TIMESTAMP_TO_YEAR(rtc_range.min);
+ int year_max = TIMESTAMP_TO_YEAR(rtc_range.max + 1) - 1;
+
+ char year[sizeof("YYYY")];
+ char month[sizeof("MM")];
+ char day[sizeof("DD")];
+ const char * dow;
+ char hour[sizeof("HH")];
+ char min[sizeof("MM")];
+ char sec[sizeof("SS")];
+
+ /* Format RTC date/time as strings */
+ sprintf( year, "%04d", CLAMP(rtc_tm.tm_year + 1900, year_min, year_max) );
+ sprintf( month, "%02d", CLAMP(rtc_tm.tm_mon + 1, 1, 12) );
+ sprintf( day, "%02d", CLAMP(rtc_tm.tm_mday, 1, 31) );
+ dow = DAYS_OF_WEEK[CLAMP(rtc_tm.tm_wday, 0, 6)];
+ sprintf( hour, "%02d", CLAMP(rtc_tm.tm_hour, 0, 23) );
+ sprintf( min, "%02d", CLAMP(rtc_tm.tm_min, 0, 59) );
+ sprintf( sec, "%02d", CLAMP(rtc_tm.tm_sec, 0, 59) );
+
+ /* Line 2 */
+ graphics_draw_text( disp, 0, LINE2, RTC_DATE_FORMAT );
+ set_edit_color( EDIT_YEAR );
+ graphics_draw_text( disp, YEAR_X, LINE2, year );
+ set_edit_color( EDIT_MONTH );
+ graphics_draw_text( disp, MONTH_X, LINE2, month );
+ set_edit_color( EDIT_DAY );
+ graphics_draw_text( disp, DAY_X, LINE2, day );
+ graphics_set_color( WHITE, BLACK );
+ graphics_draw_text( disp, DOW_X, LINE2, dow );
+
+ /* Line 3 */
+ graphics_draw_text( disp, 0, LINE3, RTC_TIME_FORMAT );
+ set_edit_color( EDIT_HOUR );
+ graphics_draw_text( disp, HOUR_X, LINE3, hour );
+ set_edit_color( EDIT_MIN );
+ graphics_draw_text( disp, MIN_X, LINE3, min );
+ set_edit_color( EDIT_SEC );
+ graphics_draw_text( disp, SEC_X, LINE3, sec );
+}
+
+static void update_joystick_directions( void )
+{
+ /* Treat the X direction as a d-pad single button press */
+ if( (pad_inputs.stick_x < -JOYSTICK_DEAD_ZONE) )
+ {
+ if( joystick_x_direction == 0 ) pad_pressed.d_left = true;
+ joystick_x_direction = -1;
+ }
+ else if ( pad_inputs.stick_x > +JOYSTICK_DEAD_ZONE )
+ {
+ if( joystick_x_direction == 0 ) pad_pressed.d_right = true;
+ joystick_x_direction = +1;
+ }
+ else joystick_x_direction = 0;
+
+ /* Treat the Y direction as a d-pad button hold */
+ if( pad_inputs.stick_y > +JOYSTICK_DEAD_ZONE )
+ {
+ pad_inputs.btn.d_up = true;
+ }
+ else if ( pad_inputs.stick_y < -JOYSTICK_DEAD_ZONE )
+ {
+ pad_inputs.btn.d_down = true;
+ }
+}
diff --git a/examples/savedoodle/.gitignore b/examples/savedoodle/.gitignore
new file mode 100644
index 000000000..ddb3723c9
--- /dev/null
+++ b/examples/savedoodle/.gitignore
@@ -0,0 +1,4 @@
+*.z64
+*.eeprom
+*.flash
+*.sram
diff --git a/examples/savedoodle/Makefile b/examples/savedoodle/Makefile
new file mode 100644
index 000000000..c0bbf83c7
--- /dev/null
+++ b/examples/savedoodle/Makefile
@@ -0,0 +1,44 @@
+all: savedoodle_eeprom4k.z64 savedoodle_eeprom16k.z64 savedoodle_sram256k.z64 \
+ savedoodle_sram768k.z64 savedoodle_sram1m.z64 savedoodle_flashram.z64
+.PHONY: all
+
+BUILD_DIR = build
+# This example uses the eeprom, flashram and sram APIs, which are preview APIs
+LIBDRAGON_PREVIEW = 2
+include $(N64_INST)/include/n64.mk
+
+OBJS = $(BUILD_DIR)/savedoodle.o
+
+# One ROM per save type: same program, different N64_ROM_SAVETYPE so emulators
+# and flashcarts mount the matching backing store. The program detects at
+# runtime which chip is actually present.
+savedoodle_eeprom4k.z64: N64_ROM_TITLE = "Doodle EEPROM 4k"
+savedoodle_eeprom4k.z64: N64_ROM_SAVETYPE = eeprom4k
+
+savedoodle_eeprom16k.z64: N64_ROM_TITLE = "Doodle EEPROM 16k"
+savedoodle_eeprom16k.z64: N64_ROM_SAVETYPE = eeprom16k
+
+savedoodle_sram256k.z64: N64_ROM_TITLE = "Doodle SRAM 256k"
+savedoodle_sram256k.z64: N64_ROM_SAVETYPE = sram256k
+
+savedoodle_sram768k.z64: N64_ROM_TITLE = "Doodle SRAM 768k"
+savedoodle_sram768k.z64: N64_ROM_SAVETYPE = sram768k
+
+savedoodle_sram1m.z64: N64_ROM_TITLE = "Doodle SRAM 1m"
+savedoodle_sram1m.z64: N64_ROM_SAVETYPE = sram1m
+
+savedoodle_flashram.z64: N64_ROM_TITLE = "Doodle FlashRAM"
+savedoodle_flashram.z64: N64_ROM_SAVETYPE = flashram
+
+$(BUILD_DIR)/savedoodle_eeprom4k.elf: $(OBJS)
+$(BUILD_DIR)/savedoodle_eeprom16k.elf: $(OBJS)
+$(BUILD_DIR)/savedoodle_sram256k.elf: $(OBJS)
+$(BUILD_DIR)/savedoodle_sram768k.elf: $(OBJS)
+$(BUILD_DIR)/savedoodle_sram1m.elf: $(OBJS)
+$(BUILD_DIR)/savedoodle_flashram.elf: $(OBJS)
+
+clean:
+ rm -rf $(BUILD_DIR) *.z64 *.v64
+.PHONY: clean
+
+-include $(wildcard $(BUILD_DIR)/*.d)
diff --git a/examples/savedoodle/savedoodle.c b/examples/savedoodle/savedoodle.c
new file mode 100644
index 000000000..c78bc2eba
--- /dev/null
+++ b/examples/savedoodle/savedoodle.c
@@ -0,0 +1,348 @@
+/**
+ * @file savedoodle.c
+ * @brief Save-backed doodle canvas
+ *
+ * A tiny "etch-a-sketch" that persists across power cycles using whatever save
+ * chip the cartridge (or emulator/flashcart) provides. The same program is
+ * built into one ROM per save type (see the Makefile); at runtime it probes the
+ * hardware to discover which backend is actually present and drives it through
+ * a uniform read/write interface.
+ *
+ * Detection is by hardware probing, not by reading the ROM header: the
+ * N64_ROM_SAVETYPE build flag only tells emulators/flashcarts which backing
+ * store to mount, it is not exposed to the running program. SRAM and FlashRAM
+ * share the same PI window (0x08000000), so probe order matters to avoid one
+ * probe corrupting the other's chip:
+ *
+ * 1. EEPROM -- lives on the separate Joybus/SI bus; probing can't disturb
+ * the PI save window, so it is always safe to check first.
+ * 2. SRAM -- sram_detect() writes one word and restores it; that write is
+ * ignored by a FlashRAM chip in its power-on read mode, so this
+ * probe is harmless to a FlashRAM cart.
+ * 3. FlashRAM -- detection writes to the command register at
+ * FLASHRAM_ADDRESS|0x10000, which on a real SRAM cart aliases
+ * back into the 32 KiB SRAM window and corrupts it. So we only
+ * probe FlashRAM once SRAM has been ruled out.
+ *
+ * Controls: D-Pad move cursor, hold A paint, hold B erase, L clear the canvas,
+ * START save, Z reload the saved doodle.
+ */
+
+#include
+#include
+
+/// Canvas dimensions in cells. 48*32 = 1536 cells stored 1 bit each = 192 bytes,
+/// so the whole save fits even the smallest supported chip (EEPROM 4Kbit = 512 B).
+#define CANVAS_W 48
+#define CANVAS_H 32
+#define CANVAS_BITS (CANVAS_W * CANVAS_H)
+#define CANVAS_BYTES (CANVAS_BITS / 8)
+
+#define STICK_DEADZONE 8 ///< Ignore small analog jitter near the stick center.
+#define STICK_SPEED 0.004f ///< Cells/frame per unit of stick deflection (-127..127).
+
+#define SAVE_MAGIC 0x444F4F44u ///< 'DOOD' -- marks a valid save in storage.
+
+/// On-disk (and in-RAM) save layout. 200 bytes, even-sized so it satisfies
+/// flashram_read()'s even-offset/length preference and fits every backend.
+typedef struct
+{
+ uint32_t magic; ///< SAVE_MAGIC when the slot holds a doodle.
+ uint32_t writes; ///< How many times this doodle has been saved.
+ uint8_t pixels[CANVAS_BYTES]; ///< 1 bit per cell, row-major.
+} save_t;
+
+_Static_assert((sizeof(save_t) & 1) == 0, "save_t must be even-sized for flashram");
+
+/// Which save backend we detected and are driving.
+typedef enum
+{
+ BACKEND_NONE = 0,
+ BACKEND_EEPROM,
+ BACKEND_SRAM,
+ BACKEND_FLASHRAM,
+} backend_t;
+
+static backend_t g_backend = BACKEND_NONE;
+static const char* g_backend_name = "none detected";
+static size_t g_capacity = 0; ///< Detected usable capacity in bytes.
+
+// --- Storage abstraction -----------------------------------------------------
+
+/// Probe the hardware and record which save chip is present. See file header
+/// for why the probe order matters.
+static void storage_detect(void)
+{
+ eeprom_type_t et = eeprom_present();
+ if (et != EEPROM_NONE)
+ {
+ g_backend = BACKEND_EEPROM;
+ g_capacity = eeprom_total_blocks() * EEPROM_BLOCK_SIZE;
+ g_backend_name = (et == EEPROM_16K) ? "EEPROM 16Kbit" : "EEPROM 4Kbit";
+ return;
+ }
+
+ sram_init();
+ int sz = sram_detect();
+ if (sz > 0)
+ {
+ g_backend = BACKEND_SRAM;
+ g_capacity = (size_t) sz;
+ g_backend_name = "SRAM";
+ return;
+ }
+
+ flashram_info_t fi;
+ if (flashram_init(NULL, &fi))
+ {
+ g_backend = BACKEND_FLASHRAM;
+ g_capacity = fi.total_size;
+ g_backend_name = fi.name; // e.g. "MX29L1101_C" or "unknown"
+ return;
+ }
+
+ g_backend = BACKEND_NONE;
+}
+
+/// Read @p len bytes at @p offset from the detected backend into @p dst.
+static void storage_read(void* dst, size_t offset, size_t len)
+{
+ switch (g_backend)
+ {
+ case BACKEND_EEPROM: eeprom_read_bytes(dst, offset, len); break;
+ case BACKEND_SRAM: sram_read(dst, offset, len); break;
+ case BACKEND_FLASHRAM: flashram_read(dst, offset, len); break;
+ default: memset(dst, 0xFF, len); break;
+ }
+}
+
+/// Write @p len bytes of @p src to @p offset on the detected backend, then block
+/// until the data has actually reached the chip.
+static void storage_write(const void* src, size_t offset, size_t len)
+{
+ switch (g_backend)
+ {
+ case BACKEND_EEPROM:
+ eeprom_write_bytes(src, offset, len);
+ eeprom_wait_idle(); // let the background flush finish before we claim "saved"
+ break;
+ case BACKEND_SRAM: sram_write(src, offset, len); break;
+ case BACKEND_FLASHRAM: flashram_write(src, offset, len); break;
+ default: break;
+ }
+}
+
+// --- Canvas ------------------------------------------------------------------
+
+/// 16-byte aligned so it is a valid PI-DMA source/destination for the SRAM and
+/// FlashRAM backends.
+static save_t g_save __attribute__((aligned(16)));
+static bool g_dirty = false; ///< Canvas changed since the last save.
+
+static inline bool cell_get(int x, int y)
+{
+ int i = y * CANVAS_W + x;
+ return (g_save.pixels[i >> 3] >> (i & 7)) & 1;
+}
+
+static inline void cell_set(int x, int y, bool on)
+{
+ int i = y * CANVAS_W + x;
+ uint8_t mask = 1 << (i & 7);
+ if (on) g_save.pixels[i >> 3] |= mask;
+ else g_save.pixels[i >> 3] &= ~mask;
+}
+
+/// Load the saved doodle. Returns true if a valid save was found; otherwise the
+/// canvas is left blank and initialised as a fresh (unwritten) save.
+static bool canvas_load(void)
+{
+ storage_read(&g_save, 0, sizeof(g_save));
+ if (g_save.magic != SAVE_MAGIC)
+ {
+ memset(&g_save, 0, sizeof(g_save));
+ g_save.magic = SAVE_MAGIC;
+ g_save.writes = 0;
+ return false;
+ }
+ return true;
+}
+
+static void canvas_save(void)
+{
+ g_save.magic = SAVE_MAGIC;
+ g_save.writes++;
+ storage_write(&g_save, 0, sizeof(g_save));
+ g_dirty = false;
+}
+
+// --- Rendering ---------------------------------------------------------------
+
+#define CELL_PX 5 ///< On-screen size of one cell.
+#define GRID_X ((320 - CANVAS_W * CELL_PX) / 2) ///< Centered horizontally.
+#define GRID_Y 64 ///< Below the header text.
+#define GRID_BOTTOM (GRID_Y + CANVAS_H * CELL_PX)
+
+#define FONT_ID 1 ///< Our registered id for the builtin debug font.
+#define STYLE_STATUS 1 ///< Highlighted style used for the status line.
+
+#define COL_BG RGBA32(20, 20, 32, 255)
+#define COL_OFF RGBA32(46, 46, 60, 255)
+#define COL_ON RGBA32(235, 235, 245, 255)
+#define COL_CURSOR RGBA32(255, 210, 0, 255)
+
+/// Draw a filled rectangle in the current fill color from (x,y) sized w*h.
+static inline void fill_box(int x, int y, int w, int h)
+{
+ rdpq_fill_rectangle(x, y, x + w, y + h);
+}
+
+static void render(int cx, int cy, const char* status)
+{
+ surface_t* disp = display_get();
+ rdpq_attach(disp, NULL);
+
+ // Background and canvas backdrop (RDP fill mode -- one solid color at a time).
+ rdpq_set_mode_fill(COL_BG);
+ fill_box(0, 0, 320, 240);
+ rdpq_set_fill_color(COL_OFF);
+ fill_box(GRID_X, GRID_Y, CANVAS_W * CELL_PX, CANVAS_H * CELL_PX);
+
+ // Lit cells, inset by 1px so the backdrop shows as a subtle grid gutter.
+ rdpq_set_fill_color(COL_ON);
+ for (int y = 0; y < CANVAS_H; y++)
+ {
+ for (int x = 0; x < CANVAS_W; x++)
+ {
+ if (cell_get(x, y))
+ fill_box(GRID_X + x * CELL_PX, GRID_Y + y * CELL_PX, CELL_PX - 1, CELL_PX - 1);
+ }
+ }
+
+ // Cursor: a cell-sized box in the cursor color with the cell state inset.
+ int cpx = GRID_X + cx * CELL_PX;
+ int cpy = GRID_Y + cy * CELL_PX;
+ rdpq_set_fill_color(COL_CURSOR);
+ fill_box(cpx, cpy, CELL_PX - 1, CELL_PX - 1);
+ rdpq_set_fill_color(cell_get(cx, cy) ? COL_ON : COL_OFF);
+ fill_box(cpx + 1, cpy + 1, CELL_PX - 3, CELL_PX - 3);
+
+ // Text (rdpq_text sets up its own render mode).
+ char cap[24];
+ if (g_capacity >= 1024)
+ sprintf(cap, "%u KiB", (unsigned)(g_capacity / 1024));
+ else
+ sprintf(cap, "%u bytes", (unsigned) g_capacity);
+
+ rdpq_text_printf(NULL, FONT_ID, 16, 16, "Backend: %s (%s)", g_backend_name, cap);
+ rdpq_text_printf(NULL, FONT_ID, 16, 28, "Saves: %u Save size: %u B %s",
+ (unsigned) g_save.writes, (unsigned) sizeof(save_t),
+ g_dirty ? "[unsaved]" : "[saved]");
+ rdpq_text_print(NULL, FONT_ID, 16, 40, "D-Pad/Stick move A paint B erase");
+ rdpq_text_print(NULL, FONT_ID, 16, 52, "L clear START save Z reload");
+
+ if (status)
+ rdpq_text_printf(&(rdpq_textparms_t){ .style_id = STYLE_STATUS },
+ FONT_ID, 16, GRID_BOTTOM + 12, "%s", status);
+
+ rdpq_detach_show();
+}
+
+// --- Main --------------------------------------------------------------------
+
+int main(void)
+{
+ debug_init_isviewer();
+ debug_init_usblog();
+
+ timer_init(); // EEPROM's background flush is timer-driven.
+ joypad_init();
+ display_init(RESOLUTION_320x240, DEPTH_32_BPP, 2, GAMMA_NONE, FILTERS_RESAMPLE);
+ rdpq_init();
+
+ // Register the builtin debug font, with a highlighted style for the status line.
+ rdpq_font_t* font = rdpq_font_load_builtin(FONT_BUILTIN_DEBUG_MONO);
+ rdpq_font_style(font, STYLE_STATUS, &(rdpq_fontstyle_t){ .color = COL_CURSOR });
+ rdpq_text_register_font(FONT_ID, font);
+
+ storage_detect();
+
+ const char* status;
+ if (g_backend == BACKEND_NONE)
+ status = "No save chip detected!";
+ else if (canvas_load())
+ status = "Loaded your saved doodle.";
+ else
+ status = "Blank canvas - draw and press START.";
+
+ // Cursor position is kept in fractional cells so the analog stick can move
+ // it with sub-cell precision; cx/cy are the resolved active cell.
+ float fx = CANVAS_W / 2;
+ float fy = CANVAS_H / 2;
+ int cx = (int) fx;
+ int cy = (int) fy;
+ int move_delay = 0;
+
+ while (1)
+ {
+ joypad_poll();
+ joypad_buttons_t pressed = joypad_get_buttons_pressed(JOYPAD_PORT_1);
+ joypad_buttons_t held = joypad_get_buttons_held(JOYPAD_PORT_1);
+ joypad_inputs_t in = joypad_get_inputs(JOYPAD_PORT_1);
+
+ // D-Pad: discrete single-cell steps, repeating while held.
+ if (move_delay > 0)
+ move_delay--;
+ if (move_delay == 0)
+ {
+ bool moved = true;
+ if (held.d_up) fy -= 1.0f;
+ else if (held.d_down) fy += 1.0f;
+ else if (held.d_left) fx -= 1.0f;
+ else if (held.d_right) fx += 1.0f;
+ else moved = false;
+ if (moved)
+ move_delay = 4;
+ }
+
+ // Analog stick: continuous movement whose speed tracks deflection, so
+ // small nudges give fine, precise control near the stick's center.
+ if (in.stick_x > STICK_DEADZONE || in.stick_x < -STICK_DEADZONE)
+ fx += in.stick_x * STICK_SPEED;
+ if (in.stick_y > STICK_DEADZONE || in.stick_y < -STICK_DEADZONE)
+ fy -= in.stick_y * STICK_SPEED; // stick up (+y) moves toward the top row
+
+ // Clamp to the canvas and resolve the active cell.
+ if (fx < 0.0f) fx = 0.0f; else if (fx > CANVAS_W - 1) fx = CANVAS_W - 1;
+ if (fy < 0.0f) fy = 0.0f; else if (fy > CANVAS_H - 1) fy = CANVAS_H - 1;
+ cx = (int) (fx + 0.5f);
+ cy = (int) (fy + 0.5f);
+
+ // Painting: hold A to draw, B to erase the cell under the cursor.
+ if (g_backend != BACKEND_NONE)
+ {
+ if (held.a && !cell_get(cx, cy)) { cell_set(cx, cy, true); g_dirty = true; }
+ if (held.b && cell_get(cx, cy)) { cell_set(cx, cy, false); g_dirty = true; }
+
+ if (pressed.l)
+ {
+ memset(g_save.pixels, 0, sizeof(g_save.pixels));
+ g_dirty = true;
+ status = "Canvas cleared (not yet saved).";
+ }
+ if (pressed.start)
+ {
+ canvas_save();
+ status = "Saved! Power-cycle to prove it persists.";
+ }
+ if (pressed.z)
+ {
+ status = canvas_load() ? "Reloaded the saved doodle."
+ : "Nothing saved yet.";
+ g_dirty = false;
+ }
+ }
+
+ render(cx, cy, status);
+ }
+}
diff --git a/examples/savedoodle/savedoodle_sram256k.ram b/examples/savedoodle/savedoodle_sram256k.ram
new file mode 100644
index 000000000..a647377d4
Binary files /dev/null and b/examples/savedoodle/savedoodle_sram256k.ram differ
diff --git a/examples/spriteanim/Makefile b/examples/spriteanim/Makefile
index 21f68572b..e1c9ee01b 100644
--- a/examples/spriteanim/Makefile
+++ b/examples/spriteanim/Makefile
@@ -24,7 +24,7 @@ spriteanim.z64: N64_ROM_TITLE="Sprite Anim Demo"
spriteanim.z64: $(BUILD_DIR)/spriteanim.dfs
clean:
- rm -rf $(BUILD_DIR) spriteanim.z64
+ rm -rf $(BUILD_DIR) spriteanim.z64 filesystem
-include $(wildcard $(BUILD_DIR)/*.d)
diff --git a/examples/spriteanim/spriteanim.c b/examples/spriteanim/spriteanim.c
index 29529e817..38e2ecf20 100644
--- a/examples/spriteanim/spriteanim.c
+++ b/examples/spriteanim/spriteanim.c
@@ -69,7 +69,7 @@ int main()
debug_init_usblog();
//Init display
- display_init(RESOLUTION_320x240, DEPTH_16_BPP, 3, GAMMA_NONE, FILTERS_RESAMPLE);
+ display_init(RESOLUTION_320x240, DEPTH_16_BPP, 3, GAMMA_NONE, FILTERS_DISABLED);
//Init DragonFS
dfs_init(DFS_DEFAULT_LOCATION);
//Init RDPQ
diff --git a/examples/test/test.c b/examples/test/test.c
index 899040a66..517c10c7e 100644
--- a/examples/test/test.c
+++ b/examples/test/test.c
@@ -38,7 +38,7 @@ sprite_t *read_sprite( const char * const spritename )
int main(void)
{
/* Initialize peripherals */
- display_init( res, bit, 2, GAMMA_NONE, FILTERS_RESAMPLE );
+ display_init( res, bit, 2, GAMMA_NONE, FILTERS_DISABLED );
dfs_init( DFS_DEFAULT_LOCATION );
joypad_init();
diff --git a/examples/videoplayer/.gitignore b/examples/videoplayer/.gitignore
new file mode 100644
index 000000000..431f5e231
--- /dev/null
+++ b/examples/videoplayer/.gitignore
@@ -0,0 +1,3 @@
+filesystem/
+*.ogv
+*.wav
diff --git a/examples/videoplayer/Makefile b/examples/videoplayer/Makefile
new file mode 100644
index 000000000..f95ab8a6f
--- /dev/null
+++ b/examples/videoplayer/Makefile
@@ -0,0 +1,78 @@
+BUILD_DIR=build
+# This example uses fmv_play and various preview APIs
+LIBDRAGON_PREVIEW = 2
+include $(N64_INST)/include/n64.mk
+
+MOVIE_FILE=caminandes.ogv
+MOVIE_URL=https://archive.org/download/CaminandesLlamigos/Caminandes_%20Llamigos-1080p.ogv
+DFS_FILES=filesystem/caminandes.h264 filesystem/caminandes.wav64 filesystem/caminandes.seek
+
+src = videoplayer.c
+
+all: videoplayer.z64
+
+videoplayer.z64: N64_ROM_TITLE="Video Player"
+videoplayer.z64: $(BUILD_DIR)/videoplayer.dfs
+
+$(BUILD_DIR)/videoplayer.elf: $(src:%.c=$(BUILD_DIR)/%.o)
+
+# If the movie file could not be encoded, build the ROM without it.
+$(BUILD_DIR)/videoplayer.dfs: $(DFS_FILES)
+ @echo " [DFS] $@"
+ if [ ! -s "$<" ]; then rm -f "$<" ; fi
+ $(N64_MKDFS) "$@" filesystem >/dev/null
+
+# Re-encode the movie's video stream if ffmpeg is available
+filesystem/caminandes.m1v: $(MOVIE_FILE)
+ @mkdir -p "$(dir $@)"
+ @echo " [VIDEO] $@"
+ if [ -s "$<" ]; then \
+ if command -v ffmpeg >/dev/null; then \
+ $(N64_VIDEOCONV) -o filesystem --codec mpeg1 --quality 70 --quick --seek 5 "$<"; \
+ else \
+ echo "videoplayer: Skipping video re-encoding because ffmpeg is not installed" ; \
+ touch "$@" ; \
+ fi ; \
+ else \
+ echo "videoplayer: Skipping video re-encoding because the sample movie is missing" ; \
+ touch "$@" ; \
+ fi
+
+# Re-encode the movie's video stream if ffmpeg is available
+filesystem/caminandes.h264: $(MOVIE_FILE)
+ @mkdir -p "$(dir $@)"
+ @echo " [VIDEO] $@"
+ if [ -s "$<" ]; then \
+ if command -v ffmpeg >/dev/null; then \
+ $(N64_VIDEOCONV) -o filesystem --codec h264 --quality 70 --quick --seek 5 "$<"; \
+ else \
+ echo "videoplayer: Skipping video re-encoding because ffmpeg is not installed" ; \
+ touch "$@" ; \
+ fi ; \
+ else \
+ echo "videoplayer: Skipping video re-encoding because the sample movie is missing" ; \
+ touch "$@" ; \
+ fi
+
+filesystem/caminandes.wav64: filesystem/caminandes.h264
+filesystem/caminandes.seek: filesystem/caminandes.h264
+
+# Download the movie file if wget or curl is available and it isn't already downloaded
+$(MOVIE_FILE):
+ if [ ! -e "$@" ]; then \
+ if command -v wget ; then \
+ wget -O "$@" "$(MOVIE_URL)" ; \
+ elif command -v curl ; then \
+ curl -Lo "$@" "$(MOVIE_URL)" ; \
+ else \
+ echo "videoplayer: Skipping sample movie download because wget or curl are not installed" ; \
+ touch "$@" ; \
+ fi ; \
+ fi
+
+clean:
+ rm -rf $(BUILD_DIR) $(DFS_FILES) videoplayer.z64
+
+-include $(wildcard $(BUILD_DIR)/*.d)
+
+.PHONY: all clean
diff --git a/examples/videoplayer/videoplayer.c b/examples/videoplayer/videoplayer.c
new file mode 100644
index 000000000..b4e1c31e4
--- /dev/null
+++ b/examples/videoplayer/videoplayer.c
@@ -0,0 +1,94 @@
+#include
+
+// Number of frame back buffers we reserve.
+// These buffers are used to render the video ahead of time.
+// More buffers help ensure smooth video playback at the cost of more memory.
+#define NUM_DISPLAY 8
+
+// Maximum target audio frequency.
+//
+// Needs to be 48 kHz if Opus audio compression is used.
+// In this example, we are using VADPCM audio compression
+// which means we can use the real frequency of the audio track.
+#define AUDIO_HZ 32000.0f
+
+
+const char *colorspace_name(yuv_colorspace_t *cs) {
+ if (memcmp(cs, &YUV_BT601_TV, sizeof(yuv_colorspace_t)) == 0) {
+ return "BT.601 TV";
+ } else if (memcmp(cs, &YUV_BT601_FULL, sizeof(yuv_colorspace_t)) == 0) {
+ return "BT.601 Full";
+ } else if (memcmp(cs, &YUV_BT709_TV, sizeof(yuv_colorspace_t)) == 0) {
+ return "BT.709 TV";
+ } else if (memcmp(cs, &YUV_BT709_FULL, sizeof(yuv_colorspace_t)) == 0) {
+ return "BT.709 Full";
+ }
+ return "Unknown";
+}
+
+int main(void)
+{
+ joypad_init();
+ debug_init_emulog();
+ debug_init_usblog();
+
+ dfs_init(DFS_DEFAULT_LOCATION);
+ rdpq_init();
+ yuv_init();
+ joypad_init();
+
+ audio_init(AUDIO_HZ, 4);
+ mixer_init(8);
+
+ video_register_codec(&mpeg1_codec);
+ video_register_codec(&h264_codec);
+
+ // Initialize profiling (FIXME)
+ profile_init(NULL);
+ void __h264_profile_init(void);
+ void __mpeg1_profile_init(void);
+
+ // Check if the movie is present in the filesystem, so that we can provide
+ // a specific error message.
+ FILE *f = fopen("rom:/caminandes.h264", "rb");
+ assertf(f, "Movie not found!\nInstall wget and ffmpeg to download and encode the sample movie\n");
+ fclose(f);
+
+ __h264_profile_init();
+ // __mpeg1_profile_init();
+
+ bool show_fps = false;
+ rdpq_font_t *fnt = rdpq_font_load_builtin(FONT_BUILTIN_DEBUG_MONO);
+ rdpq_text_register_font(10, fnt);
+
+ void osd_callback(void *ctx, int frame_idx, float time_sec, fmv_control_t *ctrl)
+ {
+ // Show debug info on first frame
+ if (frame_idx == 0) {
+ video_info_t info = video_get_info(ctrl->video);
+ debugf("Video resolution: %dx%d [DAR=%.2f] @ %.2f FPS - Colorspace: %s\n", info.width, info.height,
+ info.aspect_ratio, info.framerate, colorspace_name(&info.colorspace));
+ profile_set_target_fps(info.framerate);
+ }
+
+ // Poll input
+ joypad_poll();
+ joypad_buttons_t btn = joypad_get_buttons_pressed(JOYPAD_PORT_1);
+ if (btn.z) show_fps = !show_fps;
+ if (btn.d_right) ctrl->seek_time(ctrl, time_sec + 10.0f, false);
+ if (btn.d_left) ctrl->seek_time(ctrl, time_sec - 10.0f, false);
+
+ // Draw FPS
+ if (show_fps) {
+ rdpq_text_printf(NULL, 10, display_get_width() - 80, 15,
+ "FPS: %.02f\nTime: %02d:%02d", display_get_fps(), (int)(time_sec / 60), (int)time_sec % 60);
+ }
+
+ profile_next_frame();
+ }
+
+ fmv_play("rom:/caminandes.h264", &(fmv_parms_t){
+ .loop = true,
+ .osd_callback = osd_callback,
+ });
+}
diff --git a/examples/vifx/.gitignore b/examples/vifx/.gitignore
new file mode 100644
index 000000000..87ef66815
--- /dev/null
+++ b/examples/vifx/.gitignore
@@ -0,0 +1 @@
+filesystem/
\ No newline at end of file
diff --git a/examples/vifx/Makefile b/examples/vifx/Makefile
new file mode 100644
index 000000000..79b09dcec
--- /dev/null
+++ b/examples/vifx/Makefile
@@ -0,0 +1,27 @@
+all: vifx.z64
+.PHONY: all
+
+BUILD_DIR = build
+# This example uses the low-level VI API, which is a preview API
+LIBDRAGON_PREVIEW = 2
+include $(N64_INST)/include/n64.mk
+
+assets_png = $(wildcard assets/*.png)
+assets_conv = $(addprefix filesystem/,$(notdir $(assets_png:%.png=%.sprite)))
+OBJS = $(BUILD_DIR)/vifx.o
+
+filesystem/%.sprite: assets/%.png
+ @mkdir -p $(dir $@)
+ @echo " [SPRITE] $@"
+ @$(N64_MKSPRITE) $(MKSPRITE_FLAGS) -o filesystem "$<"
+
+vifx.z64: N64_ROM_TITLE = "VI Effects"
+vifx.z64: $(BUILD_DIR)/vifx.dfs
+$(BUILD_DIR)/vifx.dfs: $(assets_conv)
+$(BUILD_DIR)/vifx.elf: $(OBJS)
+
+clean:
+ rm -rf $(BUILD_DIR) *.z64 filesystem
+.PHONY: clean
+
+-include $(wildcard $(BUILD_DIR)/*.d)
diff --git a/examples/vifx/assets/philips.rgba32.png b/examples/vifx/assets/philips.rgba32.png
new file mode 100644
index 000000000..3d046fc72
Binary files /dev/null and b/examples/vifx/assets/philips.rgba32.png differ
diff --git a/examples/vifx/vifx.c b/examples/vifx/vifx.c
new file mode 100644
index 000000000..7dc1bb10d
--- /dev/null
+++ b/examples/vifx/vifx.c
@@ -0,0 +1,73 @@
+#include
+
+#define MAX(a, b) ((a) > (b) ? (a) : (b))
+#define MIN(a, b) ((a) < (b) ? (a) : (b))
+
+int main(void)
+{
+ debug_init_emulog();
+ debug_init_usblog();
+ joypad_init();
+
+ vi_init();
+ dfs_init(DFS_DEFAULT_LOCATION);
+ rdpq_init();
+ rdpq_debug_start();
+
+ sprite_t *bkg = sprite_load("rom:/philips.rgba32.sprite");
+ surface_t fb1 = surface_alloc(FMT_RGBA16, 320, 240);
+ surface_t fb2 = surface_alloc(FMT_RGBA16, 640, 480);
+
+ rdpq_attach(&fb1, NULL);
+ rdpq_set_mode_standard();
+ rdpq_sprite_blit(bkg, 0, 0, &(rdpq_blitparms_t){
+ .scale_x = 0.5f, .scale_y = 0.5f,
+ });
+ rdpq_detach_wait();
+
+ rdpq_attach(&fb2, NULL);
+ rdpq_set_mode_standard();
+ rdpq_sprite_blit(bkg, 0, 0, &(rdpq_blitparms_t){});
+ rdpq_detach_wait();
+
+ vi_show(&fb1);
+
+ bool interlacing = false;
+ bool hires = false;
+ bool borders = false;
+
+ while (1) {
+ joypad_poll();
+ joypad_buttons_t btn = joypad_get_buttons_pressed(JOYPAD_PORT_1);
+ joypad_buttons_t down = joypad_get_buttons_held(JOYPAD_PORT_1);
+
+ vi_write_begin();
+
+ if (btn.a) {
+ borders = !borders;
+ vi_set_borders(vi_calc_borders(4.0f / 3.0f, borders ? VI_CRT_MARGIN : 0));
+ surface_t *fb = hires ? &fb2 : &fb1;
+ vi_set_xscale(fb->width);
+ vi_set_yscale(fb->height);
+ }
+ if (btn.b) {
+ hires = !hires;
+ debugf("HiRes: %d\n", hires);
+ vi_show(hires ? &fb2 : &fb1);
+ borders = false;
+ }
+ if (btn.z) {
+ interlacing = !interlacing;
+ debugf("Interlacing: %d\n", interlacing);
+ vi_set_interlaced(interlacing);
+ }
+
+ if (down.c_up) vi_scroll_output(0, -1);
+ if (down.c_down) vi_scroll_output(0, 1);
+ if (down.c_left) vi_scroll_output(-1, 0);
+ if (down.c_right) vi_scroll_output(1, 0);
+ vi_write_end();
+
+ vi_wait_vblank();
+ }
+}
\ No newline at end of file
diff --git a/examples/vrutest/Makefile b/examples/vrutest/Makefile
deleted file mode 100644
index bc2bc02b4..000000000
--- a/examples/vrutest/Makefile
+++ /dev/null
@@ -1,17 +0,0 @@
-all: vrutest.z64
-.PHONY: all
-
-BUILD_DIR = build
-include $(N64_INST)/include/n64.mk
-
-OBJS = $(BUILD_DIR)/vrutest.o
-
-vrutest.z64: N64_ROM_TITLE = "VRU Test"
-
-$(BUILD_DIR)/vrutest.elf: $(OBJS)
-
-clean:
- rm -rf $(BUILD_DIR) *.z64
-.PHONY: clean
-
--include $(wildcard $(BUILD_DIR)/*.d)
diff --git a/examples/vrutest/vrutest.c b/examples/vrutest/vrutest.c
deleted file mode 100644
index b2babbe7b..000000000
--- a/examples/vrutest/vrutest.c
+++ /dev/null
@@ -1,161 +0,0 @@
-#include
-#include
-#include
-#include
-#include
-
-void print_request( int len, uint8_t *res )
-{
- for( int i = 0; i < len; i++ )
- {
- printf( "%02X", res[i] );
- }
-
- printf( " " );
-}
-
-void print_result( int len, uint8_t *res )
-{
- for( int i = 0; i < len; i++ )
- {
- printf( "%02X", res[i] );
- }
-
- printf( "\n" );
-}
-
-int main(void)
-{
- /* Initialize peripherals */
- console_init();
- controller_init();
-
- console_set_render_mode(RENDER_MANUAL);
-
- /* Main loop test */
- while(1)
- {
- console_clear();
-
- /* To do initialize routines */
- controller_scan();
-
- int controllers = get_controllers_present();
-
- if( controllers & CONTROLLER_1_INSERTED )
- {
- struct controller_data output;
- int accessories = get_accessories_present( &output );
-
- if( (accessories & CONTROLLER_4_INSERTED) && identify_accessory( 3 ) == ACCESSORY_VRU )
- {
- uint8_t out[64];
- uint8_t in[64];
-
- // Initial sequence
- out[0] = 0x00;
- out[1] = 0x00;
- print_request( 2, out );
- execute_raw_command( 3, 0xB, 2, 3, out, in );
- print_result( 3, in );
-
- // LODS OF 0x0B
- out[0] = 0x1E;
- out[1] = 0x0C;
- print_request( 2, out );
- execute_raw_command( 3, 0xD, 2, 1, out, in );
- print_result( 1, in );
-
- out[0] = 0x6E;
- out[1] = 0x07;
- print_request( 2, out );
- execute_raw_command( 3, 0xD, 2, 1, out, in );
- print_result( 1, in );
-
- out[0] = 0x08;
- out[1] = 0x0E;
- print_request( 2, out );
- execute_raw_command( 3, 0xD, 2, 1, out, in );
- print_result( 1, in );
-
- out[0] = 0x56;
- out[1] = 0x18;
- print_request( 2, out );
- execute_raw_command( 3, 0xD, 2, 1, out, in );
- print_result( 1, in );
-
- out[0] = 0x03;
- out[1] = 0x0F;
- print_request( 2, out );
- execute_raw_command( 3, 0xD, 2, 1, out, in );
- print_result( 1, in );
-
- // Some C's and B's
- out[0] = 0x00;
- out[1] = 0x00;
- out[2] = 0x00;
- out[3] = 0x00;
- out[4] = 0x01;
- out[5] = 0x00;
- print_request( 6, out );
- execute_raw_command( 3, 0xC, 6, 1, out, in );
- print_result( 1, in );
-
- out[0] = 0x00;
- out[1] = 0x00;
- print_request( 2, out );
- execute_raw_command( 3, 0xB, 2, 3, out, in );
- print_result( 3, in );
-
- out[0] = 0x00;
- out[1] = 0x00;
- out[2] = 0x02;
- out[3] = 0x00;
- out[4] = 0x3B;
- out[5] = 0x00;
- print_request( 6, out );
- execute_raw_command( 3, 0xC, 6, 1, out, in );
- print_result( 1, in );
-
- out[0] = 0x00;
- out[1] = 0x00;
- print_request( 2, out );
- execute_raw_command( 3, 0xB, 2, 3, out, in );
- print_result( 3, in );
-
- // Give that A a try
- memset( out, 0, sizeof( out ) );
- print_request( 22, out );
- execute_raw_command( 3, 0xA, 22, 1, out, in );
- print_result( 1, in );
-
- memset( out, 0, sizeof( out ) );
- print_request( 22, out );
- execute_raw_command( 3, 0xA, 22, 1, out, in );
- print_result( 1, in );
-
- memset( out, 0, sizeof( out ) );
- out[14] = 0x03;
- out[18] = 0x12;
- out[20] = 0x08;
- print_request( 22, out );
- execute_raw_command( 3, 0xA, 22, 1, out, in );
- print_result( 1, in );
-
- // Render it!
- console_render();
- while(1){;}
- }
- else
- {
- printf( "Please insert a VRU into slot 4.\n" );
- }
- }
- else
- {
- printf( "Please insert a standard\ncontroller into slot 1.\n" );
- }
-
- console_render();
- }
-}
diff --git a/include/GL/gl.h b/include/GL/gl.h
new file mode 100644
index 000000000..c1bb1c62c
--- /dev/null
+++ b/include/GL/gl.h
@@ -0,0 +1,680 @@
+/**
+ * @file gl.h
+ * @brief OpenGL 1.1 implementation
+ * @author Dennis Heinze
+ * @preview
+ */
+#ifndef __LIBDRAGON_GL_H
+#define __LIBDRAGON_GL_H
+
+
+#include
+#include
+#include "preview.h"
+LIBDRAGON_PREVIEW_HEADER
+
+typedef struct surface_s surface_t;
+typedef struct sprite_s sprite_t;
+typedef struct rdpq_texparms_s rdpq_texparms_t;
+
+#include
+#include
+
+#define _GL_UNSUPPORTED(func) _Static_assert(0, #func " is not supported!")
+
+#define GL_VERSION_1_1 1
+#define GL_ARB_multisample 1
+#define GL_EXT_packed_pixels 1
+#define GL_ARB_vertex_buffer_object 1
+#define GL_ARB_texture_mirrored_repeat 1
+#define GL_ARB_vertex_array_object 1
+#define GL_ARB_matrix_palette 1
+#define GL_N64_RDPQ_interop 1
+#define GL_N64_surface_image 1
+#define GL_N64_half_fixed_point 1
+#define GL_N64_reduced_aliasing 1
+#define GL_N64_interpenetrating 1
+#define GL_N64_dither_mode 1
+#define GL_N64_copy_matrix 1
+#define GL_N64_texture_flip 1
+
+/* Data types */
+
+typedef uint8_t GLboolean;
+typedef int8_t GLbyte;
+typedef uint8_t GLubyte;
+typedef int16_t GLshort;
+typedef uint16_t GLushort;
+typedef int32_t GLint;
+typedef uint32_t GLuint;
+typedef uint32_t GLsizei;
+typedef uint32_t GLenum;
+typedef uint32_t GLbitfield;
+typedef float GLfloat;
+typedef float GLclampf;
+typedef double GLdouble;
+typedef double GLclampd;
+typedef void GLvoid;
+
+typedef intptr_t GLintptrARB;
+typedef size_t GLsizeiptrARB;
+
+typedef int16_t GLhalfxN64;
+
+#define GL_FALSE 0
+#define GL_TRUE 1
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Errors */
+
+GLenum glGetError(void);
+
+/* Flags */
+
+void glEnable(GLenum target);
+void glDisable(GLenum target);
+
+/* Immediate mode */
+
+void glBegin(GLenum mode);
+void glEnd(void);
+
+#define glEdgeFlag(flag) _GL_UNSUPPORTED(glEdgeFlag)
+#define glEdgeFlagv(flag) _GL_UNSUPPORTED(glEdgeFlagv)
+
+void glVertex2s(GLshort x, GLshort y);
+void glVertex2i(GLint x, GLint y);
+void glVertex2f(GLfloat x, GLfloat y);
+void glVertex2d(GLdouble x, GLdouble y);
+void glVertex2hxN64(GLhalfxN64 x, GLhalfxN64 y);
+
+void glVertex3s(GLshort x, GLshort y, GLshort z);
+void glVertex3i(GLint x, GLint y, GLint z);
+void glVertex3f(GLfloat x, GLfloat y, GLfloat z);
+void glVertex3d(GLdouble x, GLdouble y, GLdouble z);
+void glVertex3hxN64(GLhalfxN64 x, GLhalfxN64 y, GLhalfxN64 z);
+
+void glVertex4s(GLshort x, GLshort y, GLshort z, GLshort w);
+void glVertex4i(GLint x, GLint y, GLint z, GLint w);
+void glVertex4f(GLfloat x, GLfloat y, GLfloat z, GLfloat w);
+void glVertex4d(GLdouble x, GLdouble y, GLdouble z, GLdouble w);
+void glVertex4hxN64(GLhalfxN64 x, GLhalfxN64 y, GLhalfxN64 z, GLhalfxN64 w);
+
+void glVertex2sv(const GLshort *v);
+void glVertex2iv(const GLint *v);
+void glVertex2fv(const GLfloat *v);
+void glVertex2dv(const GLdouble *v);
+void glVertex2hxvN64(const GLhalfxN64 *v);
+
+void glVertex3sv(const GLshort *v);
+void glVertex3iv(const GLint *v);
+void glVertex3fv(const GLfloat *v);
+void glVertex3dv(const GLdouble *v);
+void glVertex3hxvN64(const GLhalfxN64 *v);
+
+void glVertex4sv(const GLshort *v);
+void glVertex4iv(const GLint *v);
+void glVertex4fv(const GLfloat *v);
+void glVertex4dv(const GLdouble *v);
+void glVertex4hxvN64(const GLhalfxN64 *v);
+
+void glTexCoord1s(GLshort s);
+void glTexCoord1i(GLint s);
+void glTexCoord1f(GLfloat s);
+void glTexCoord1d(GLdouble s);
+void glTexCoord1hxN64(GLhalfxN64 s);
+
+void glTexCoord2s(GLshort s, GLshort t);
+void glTexCoord2i(GLint s, GLint t);
+void glTexCoord2f(GLfloat s, GLfloat t);
+void glTexCoord2d(GLdouble s, GLdouble t);
+void glTexCoord2hxN64(GLhalfxN64 s, GLhalfxN64 t);
+
+void glTexCoord3s(GLshort s, GLshort t, GLshort r);
+void glTexCoord3i(GLint s, GLint t, GLint r);
+void glTexCoord3f(GLfloat s, GLfloat t, GLfloat r);
+void glTexCoord3d(GLdouble s, GLdouble t, GLdouble r);
+void glTexCoord3hxN64(GLhalfxN64 s, GLhalfxN64 t, GLhalfxN64 r);
+
+void glTexCoord4s(GLshort s, GLshort t, GLshort r, GLshort q);
+void glTexCoord4i(GLint s, GLint t, GLint r, GLint q);
+void glTexCoord4f(GLfloat s, GLfloat t, GLfloat r, GLfloat q);
+void glTexCoord4d(GLdouble s, GLdouble t, GLdouble r, GLdouble q);
+void glTexCoord4hxN64(GLhalfxN64 s, GLhalfxN64 t, GLhalfxN64 r, GLhalfxN64 q);
+
+void glTexCoord1sv(const GLshort *v);
+void glTexCoord1iv(const GLint *v);
+void glTexCoord1fv(const GLfloat *v);
+void glTexCoord1dv(const GLdouble *v);
+void glTexCoord1hxvN64(const GLhalfxN64 *v);
+
+void glTexCoord2sv(const GLshort *v);
+void glTexCoord2iv(const GLint *v);
+void glTexCoord2fv(const GLfloat *v);
+void glTexCoord2dv(const GLdouble *v);
+void glTexCoord2hxvN64(const GLhalfxN64 *v);
+
+void glTexCoord3sv(const GLshort *v);
+void glTexCoord3iv(const GLint *v);
+void glTexCoord3fv(const GLfloat *v);
+void glTexCoord3dv(const GLdouble *v);
+void glTexCoord3hxvN64(const GLhalfxN64 *v);
+
+void glTexCoord4sv(const GLshort *v);
+void glTexCoord4iv(const GLint *v);
+void glTexCoord4fv(const GLfloat *v);
+void glTexCoord4dv(const GLdouble *v);
+void glTexCoord4hxvN64(const GLhalfxN64 *v);
+
+void glNormal3b(GLbyte nx, GLbyte ny, GLbyte nz);
+void glNormal3s(GLshort nx, GLshort ny, GLshort nz);
+void glNormal3i(GLint nx, GLint ny, GLint nz);
+void glNormal3f(GLfloat nx, GLfloat ny, GLfloat nz);
+void glNormal3d(GLdouble nx, GLdouble ny, GLdouble nz);
+
+void glNormal3bv(const GLbyte *v);
+void glNormal3sv(const GLshort *v);
+void glNormal3iv(const GLint *v);
+void glNormal3fv(const GLfloat *v);
+void glNormal3dv(const GLdouble *v);
+
+void glColor3b(GLbyte r, GLbyte g, GLbyte b);
+void glColor3s(GLshort r, GLshort g, GLshort b);
+void glColor3i(GLint r, GLint g, GLint b);
+void glColor3f(GLfloat r, GLfloat g, GLfloat b);
+void glColor3d(GLdouble r, GLdouble g, GLdouble b);
+void glColor3ub(GLubyte r, GLubyte g, GLubyte b);
+void glColor3us(GLushort r, GLushort g, GLushort b);
+void glColor3ui(GLuint r, GLuint g, GLuint b);
+
+void glColor4b(GLbyte r, GLbyte g, GLbyte b, GLbyte a);
+void glColor4s(GLshort r, GLshort g, GLshort b, GLshort a);
+void glColor4i(GLint r, GLint g, GLint b, GLint a);
+void glColor4f(GLfloat r, GLfloat g, GLfloat b, GLfloat a);
+void glColor4d(GLdouble r, GLdouble g, GLdouble b, GLdouble a);
+void glColor4ub(GLubyte r, GLubyte g, GLubyte b, GLubyte a);
+void glColor4us(GLushort r, GLushort g, GLushort b, GLushort a);
+void glColor4ui(GLuint r, GLuint g, GLuint b, GLuint a);
+
+void glColor3bv(const GLbyte *v);
+void glColor3sv(const GLshort *v);
+void glColor3iv(const GLint *v);
+void glColor3fv(const GLfloat *v);
+void glColor3dv(const GLdouble *v);
+void glColor3ubv(const GLubyte *v);
+void glColor3usv(const GLushort *v);
+void glColor3uiv(const GLuint *v);
+
+void glColor4bv(const GLbyte *v);
+void glColor4sv(const GLshort *v);
+void glColor4iv(const GLint *v);
+void glColor4fv(const GLfloat *v);
+void glColor4dv(const GLdouble *v);
+void glColor4ubv(const GLubyte *v);
+void glColor4usv(const GLushort *v);
+void glColor4uiv(const GLuint *v);
+
+#define glIndexs(c) _GL_UNSUPPORTED(glIndexs)
+#define glIndexi(c) _GL_UNSUPPORTED(glIndexi)
+#define glIndexf(c) _GL_UNSUPPORTED(glIndexf)
+#define glIndexd(c) _GL_UNSUPPORTED(glIndexd)
+#define glIndexub(c) _GL_UNSUPPORTED(glIndexub)
+#define glIndexsv(v) _GL_UNSUPPORTED(glIndexsv)
+#define glIndexiv(v) _GL_UNSUPPORTED(glIndexiv)
+#define glIndexfv(v) _GL_UNSUPPORTED(glIndexfv)
+#define glIndexdv(v) _GL_UNSUPPORTED(glIndexdv)
+#define glIndexubv(v) _GL_UNSUPPORTED(glIndexubv)
+
+void glMatrixIndexubvARB(GLint size, const GLubyte *v);
+void glMatrixIndexusvARB(GLint size, const GLushort *v);
+void glMatrixIndexuivARB(GLint size, const GLuint *v);
+
+/* Fixed point */
+
+void glVertexHalfFixedPrecisionN64(GLuint bits);
+void glTexCoordHalfFixedPrecisionN64(GLuint bits);
+
+/* Vertex arrays */
+
+void glVertexPointer(GLint size, GLenum type, GLsizei stride, const GLvoid *pointer);
+void glTexCoordPointer(GLint size, GLenum type, GLsizei stride, const GLvoid *pointer);
+void glNormalPointer(GLenum type, GLsizei stride, const GLvoid *pointer);
+void glColorPointer(GLint size, GLenum type, GLsizei stride, const GLvoid *pointer);
+void glMatrixIndexPointerARB(GLint size, GLenum type, GLsizei stride, const GLvoid *pointer);
+
+#define glEdgeFlagPointer(stride, pointer) _GL_UNSUPPORTED(glEdgeFlagPointer)
+#define glIndexPointer(type, stride, pointer) _GL_UNSUPPORTED(glIndexPointer)
+
+void glEnableClientState(GLenum array);
+void glDisableClientState(GLenum array);
+
+void glArrayElement(GLint i);
+
+void glDrawArrays(GLenum mode, GLint first, GLsizei count);
+
+void glDrawElements(GLenum mode, GLsizei count, GLenum type, const GLvoid *indices);
+
+void glInterleavedArrays(GLenum format, GLsizei stride, const GLvoid *pointer);
+
+void glGenVertexArrays(GLsizei n, GLuint *arrays);
+void glDeleteVertexArrays(GLsizei n, const GLuint *arrays);
+
+void glBindVertexArray(GLuint array);
+
+GLboolean glIsVertexArray(GLuint array);
+
+/* Buffer Objects */
+
+void glBindBufferARB(GLenum target, GLuint buffer);
+void glDeleteBuffersARB(GLsizei n, const GLuint *buffers);
+void glGenBuffersARB(GLsizei n, GLuint *buffers);
+GLboolean glIsBufferARB(GLuint buffer);
+
+void glBufferDataARB(GLenum target, GLsizeiptrARB size, const GLvoid *data, GLenum usage);
+void glBufferSubDataARB(GLenum target, GLintptrARB offset, GLsizeiptrARB size, const GLvoid *data);
+
+void glGetBufferSubDataARB(GLenum target, GLintptrARB offset, GLsizeiptrARB size, GLvoid *data);
+
+GLvoid * glMapBufferARB(GLenum target, GLenum access);
+GLboolean glUnmapBufferARB(GLenum target);
+
+void glGetBufferParameterivARB(GLenum target, GLenum pname, GLint *params);
+void glGetBufferPointervARB(GLenum target, GLenum pname, GLvoid **params);
+
+/* Rectangles */
+
+void glRects(GLshort x1, GLshort y1, GLshort x2, GLshort y2);
+void glRecti(GLint x1, GLint y1, GLint x2, GLint y2);
+void glRectf(GLfloat x1, GLfloat y1, GLfloat x2, GLfloat y2);
+void glRectd(GLdouble x1, GLdouble y1, GLdouble x2, GLdouble y2);
+
+void glRectsv(const GLshort *v1, const GLshort *v2);
+void glRectiv(const GLint *v1, const GLint *v2);
+void glRectfv(const GLfloat *v1, const GLfloat *v2);
+void glRectdv(const GLdouble *v1, const GLdouble *v2);
+
+/* Viewport */
+
+void glDepthRange(GLclampd n, GLclampd f);
+
+void glViewport(GLint x, GLint y, GLsizei w, GLsizei h);
+
+/* Matrices */
+
+void glMatrixMode(GLenum mode);
+
+void glLoadMatrixf(const GLfloat *m);
+void glLoadMatrixd(const GLdouble *m);
+
+void glMultMatrixf(const GLfloat *m);
+void glMultMatrixd(const GLdouble *m);
+
+void glLoadIdentity(void);
+
+void glRotatef(GLfloat angle, GLfloat x, GLfloat y, GLfloat z);
+void glRotated(GLdouble angle, GLdouble x, GLdouble y, GLdouble z);
+
+void glTranslatef(GLfloat x, GLfloat y, GLfloat z);
+void glTranslated(GLdouble x, GLdouble y, GLdouble z);
+
+void glScalef(GLfloat x, GLfloat y, GLfloat z);
+void glScaled(GLdouble x, GLdouble y, GLdouble z);
+
+void glFrustum(GLdouble l, GLdouble r, GLdouble b, GLdouble t, GLdouble n, GLdouble f);
+
+void glOrtho(GLdouble l, GLdouble r, GLdouble b, GLdouble t, GLdouble n, GLdouble f);
+
+void glPushMatrix(void);
+void glPopMatrix(void);
+
+void glCurrentPaletteMatrixARB(GLint index);
+
+void glCopyMatrixN64(GLenum source);
+
+/* Texture coordinate generation */
+
+void glTexGeni(GLenum coord, GLenum pname, GLint param);
+void glTexGenf(GLenum coord, GLenum pname, GLfloat param);
+void glTexGend(GLenum coord, GLenum pname, GLdouble param);
+
+void glTexGeniv(GLenum coord, GLenum pname, const GLint *params);
+void glTexGenfv(GLenum coord, GLenum pname, const GLfloat *params);
+void glTexGendv(GLenum coord, GLenum pname, const GLdouble *params);
+
+/* Clipping planes */
+
+#define glClipPlane(p, eqn) _GL_UNSUPPORTED(glClipPlane)
+
+/* Raster position */
+
+#define glRasterPos2s(x, y) _GL_UNSUPPORTED(glRasterPos2s)
+#define glRasterPos2i(x, y) _GL_UNSUPPORTED(glRasterPos2i)
+#define glRasterPos2f(x, y) _GL_UNSUPPORTED(glRasterPos2f)
+#define glRasterPos2d(x, y) _GL_UNSUPPORTED(glRasterPos2d)
+#define glRasterPos3s(x, y, z) _GL_UNSUPPORTED(glRasterPos3s)
+#define glRasterPos3i(x, y, z) _GL_UNSUPPORTED(glRasterPos3i)
+#define glRasterPos3f(x, y, z) _GL_UNSUPPORTED(glRasterPos3f)
+#define glRasterPos3d(x, y, z) _GL_UNSUPPORTED(glRasterPos3d)
+#define glRasterPos4s(x, y, z, w) _GL_UNSUPPORTED(glRasterPos4s)
+#define glRasterPos4i(x, y, z, w) _GL_UNSUPPORTED(glRasterPos4i)
+#define glRasterPos4f(x, y, z, w) _GL_UNSUPPORTED(glRasterPos4f)
+#define glRasterPos4d(x, y, z, w) _GL_UNSUPPORTED(glRasterPos4d)
+#define glRasterPos2sv(v) _GL_UNSUPPORTED(glRasterPos2sv)
+#define glRasterPos2iv(v) _GL_UNSUPPORTED(glRasterPos2iv)
+#define glRasterPos2fv(v) _GL_UNSUPPORTED(glRasterPos2fv)
+#define glRasterPos2dv(v) _GL_UNSUPPORTED(glRasterPos2dv)
+#define glRasterPos3sv(v) _GL_UNSUPPORTED(glRasterPos3sv)
+#define glRasterPos3iv(v) _GL_UNSUPPORTED(glRasterPos3iv)
+#define glRasterPos3fv(v) _GL_UNSUPPORTED(glRasterPos3fv)
+#define glRasterPos3dv(v) _GL_UNSUPPORTED(glRasterPos3dv)
+#define glRasterPos4sv(v) _GL_UNSUPPORTED(glRasterPos4sv)
+#define glRasterPos4iv(v) _GL_UNSUPPORTED(glRasterPos4iv)
+#define glRasterPos4fv(v) _GL_UNSUPPORTED(glRasterPos4fv)
+#define glRasterPos4dv(v) _GL_UNSUPPORTED(glRasterPos4dv)
+
+/* Shading and lighting */
+
+void glMateriali(GLenum face, GLenum pname, GLint param);
+void glMaterialf(GLenum face, GLenum pname, GLfloat param);
+
+void glMaterialiv(GLenum face, GLenum pname, const GLint *params);
+void glMaterialfv(GLenum face, GLenum pname, const GLfloat *params);
+
+void glLighti(GLenum light, GLenum pname, GLint param);
+void glLightf(GLenum light, GLenum pname, GLfloat param);
+
+void glLightiv(GLenum light, GLenum pname, const GLint *params);
+void glLightfv(GLenum light, GLenum pname, const GLfloat *params);
+
+void glLightModeli(GLenum pname, GLint param);
+void glLightModelf(GLenum pname, GLfloat param);
+
+void glLightModeliv(GLenum pname, const GLint *params);
+void glLightModelfv(GLenum pname, const GLfloat *params);
+
+void glColorMaterial(GLenum face, GLenum mode);
+
+void glShadeModel(GLenum mode);
+
+/* Points */
+
+void glPointSize(GLfloat size);
+
+/* Lines */
+
+void glLineWidth(GLfloat width);
+
+#define glLineStipple(factor, pattern) _GL_UNSUPPORTED(glLineStipple)
+
+/* Polygons */
+
+void glCullFace(GLenum mode);
+
+void glFrontFace(GLenum dir);
+
+void glPolygonMode(GLenum face, GLenum mode);
+
+#define glPolygonStipple(pattern) _GL_UNSUPPORTED(glPolygonStipple)
+#define glPolygonOffset(factor, units) _GL_UNSUPPORTED(glPolygonOffset)
+
+/* Pixel rectangles */
+
+void glPixelStorei(GLenum pname, GLint param);
+void glPixelStoref(GLenum pname, GLfloat param);
+
+void glPixelTransferi(GLenum pname, GLint value);
+void glPixelTransferf(GLenum pname, GLfloat value);
+
+void glPixelMapusv(GLenum map, GLsizei size, const GLushort *values);
+void glPixelMapuiv(GLenum map, GLsizei size, const GLuint *values);
+void glPixelMapfv(GLenum map, GLsizei size, const GLfloat *values);
+
+#define glPixelZoom(zx, zy) _GL_UNSUPPORTED(glPixelZoom)
+#define glDrawPixels(width, height, format, type, data) _GL_UNSUPPORTED(glDrawPixels)
+#define glReadPixels(x, y, width, height, format, type, data) _GL_UNSUPPORTED(glReadPixels)
+#define glCopyPixels(x, y, width, height, type) _GL_UNSUPPORTED(glCopyPixels)
+
+/* Bitmaps */
+
+#define glBitmap(w, h, xbo, ybo, xbi, ybi, data) _GL_UNSUPPORTED(glBitmap)
+
+/* Texturing */
+
+void glTexImage1D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLint border, GLenum format, GLenum type, const GLvoid *data);
+void glTexImage2D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const GLvoid *data);
+
+void glSurfaceTexImageN64(GLenum target, GLint level, surface_t *surface, rdpq_texparms_t *texparms);
+void glSpriteTextureN64(GLenum target, sprite_t *sprite, rdpq_texparms_t *texparms);
+
+void glTexParameteri(GLenum target, GLenum pname, GLint param);
+void glTexParameterf(GLenum target, GLenum pname, GLfloat param);
+
+void glTexParameteriv(GLenum target, GLenum pname, const GLint *params);
+void glTexParameterfv(GLenum target, GLenum pname, const GLfloat *params);
+
+void glBindTexture(GLenum target, GLuint texture);
+
+void glDeleteTextures(GLsizei n, const GLuint *textures);
+void glGenTextures(GLsizei n, GLuint *textures);
+
+GLboolean glAreTexturesResident(GLsizei n, const GLuint *textures, const GLboolean *residences);
+
+void glPrioritizeTextures(GLsizei n, const GLuint *textures, const GLclampf *priorities);
+
+void glTexEnvi(GLenum target, GLenum pname, GLint param);
+void glTexEnvf(GLenum target, GLenum pname, GLfloat param);
+
+void glTexEnviv(GLenum target, GLenum pname, const GLint *params);
+void glTexEnvfv(GLenum target, GLenum pname, const GLfloat *params);
+
+GLboolean glIsTexture(GLuint texture);
+
+#define glCopyTexImage1D(target, level, internalformat, x, y, width, border) _GL_UNSUPPORTED(glCopyTexImage1D)
+#define glCopyTexImage2D(target, level, internalformat, x, y, width, height, border) _GL_UNSUPPORTED(glCopyTexImage2D)
+#define glTexSubImage1D(target, level, xoffset, width, format, type, data) _GL_UNSUPPORTED(glTexSubImage1D)
+#define glTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, data) _GL_UNSUPPORTED(glTexSubImage2D)
+#define glCopyTexSubImage1D(target, level, xoffset, x, y, width) _GL_UNSUPPORTED(glCopyTexSubImage1D)
+#define glCopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height) _GL_UNSUPPORTED(glCopyTexSubImage2D)
+
+void glTexSizeN64(GLushort width, GLushort height);
+
+/* Fog */
+
+void glFogi(GLenum pname, GLint param);
+void glFogf(GLenum pname, GLfloat param);
+
+void glFogiv(GLenum pname, const GLint *params);
+void glFogfv(GLenum pname, const GLfloat *params);
+
+/* Dithering */
+
+void glDitherModeN64(rdpq_dither_t mode);
+
+/* Scissor test */
+
+void glScissor(GLint left, GLint bottom, GLsizei width, GLsizei height);
+
+/* Alpha test */
+
+void glAlphaFunc(GLenum func, GLclampf ref);
+
+/* Stencil test */
+
+#define glStencilFunc(func, ref, mask) _GL_UNSUPPORTED(glStencilFunc)
+#define glStencilOp(sfail, dpfail, dppass) _GL_UNSUPPORTED(glStencilOp)
+
+/* Depth test */
+
+void glDepthFunc(GLenum func);
+
+/* Blending */
+
+void glBlendFunc(GLenum src, GLenum dst);
+
+/* Logical operation */
+
+#define glLogicOp(op) _GL_UNSUPPORTED(glLogicOp)
+
+/* Framebuffer selection */
+
+#define glDrawBuffer(buf) _GL_UNSUPPORTED(glDrawBuffer)
+#define glReadBuffer(src) _GL_UNSUPPORTED(glReadBuffer)
+
+/* Masks */
+
+void glDepthMask(GLboolean mask);
+
+#define glIndexMask(mask) _GL_UNSUPPORTED(glIndexMask)
+#define glColorMask(r, g, b, a) _GL_UNSUPPORTED(glColorMask)
+#define glStencilMask(mask) _GL_UNSUPPORTED(glStencilMask)
+
+/* Clearing */
+
+void glClear(GLbitfield buf);
+
+void glClearColor(GLclampf r, GLclampf g, GLclampf b, GLclampf a);
+void glClearDepth(GLclampd d);
+
+#define glClearIndex(index) _GL_UNSUPPORTED(glClearIndex)
+#define glClearStencil(s) _GL_UNSUPPORTED(glClearStencil)
+#define glClearAccum(r, g, b, a) _GL_UNSUPPORTED(glClearAccum)
+
+/* Accumulation buffer */
+
+#define glAccum(op, value) _GL_UNSUPPORTED(glAccum)
+
+/* Evaluators */
+
+#define glMap1f(type, u1, u2, stride, order, points) _GL_UNSUPPORTED(glMap1f)
+#define glMap1d(type, u1, u2, stride, order, points) _GL_UNSUPPORTED(glMap1d)
+#define glMap2f(target, u1, u2, ustride, uorder, v1, v2, vstride, vorder, points) _GL_UNSUPPORTED(glMap2f)
+#define glMap2d(target, u1, u2, ustride, uorder, v1, v2, vstride, vorder, points) _GL_UNSUPPORTED(glMap2d)
+#define glEvalCoord1f(u) _GL_UNSUPPORTED(glEvalCoord1f)
+#define glEvalCoord1d(u) _GL_UNSUPPORTED(glEvalCoord1d)
+#define glEvalCoord2f(u, v) _GL_UNSUPPORTED(glEvalCoord2f)
+#define glEvalCoord2d(u, v) _GL_UNSUPPORTED(glEvalCoord2d)
+#define glEvalCoord1fv(v) _GL_UNSUPPORTED(glEvalCoord1fv)
+#define glEvalCoord1dv(v) _GL_UNSUPPORTED(glEvalCoord1dv)
+#define glEvalCoord2fv(v) _GL_UNSUPPORTED(glEvalCoord2fv)
+#define glEvalCoord2dv(v) _GL_UNSUPPORTED(glEvalCoord2dv)
+#define glMapGrid1f(n, u1, u2) _GL_UNSUPPORTED(glMapGrid1f)
+#define glMapGrid1d(n, u1, u2) _GL_UNSUPPORTED(glMapGrid1d)
+#define glMapGrid2f(nu, u1, u2, nv, v1, v2) _GL_UNSUPPORTED(glMapGrid2f)
+#define glMapGrid2d(nu, u1, u2, nv, v1, v2) _GL_UNSUPPORTED(glMapGrid2d)
+#define glEvalMesh1(mode, p1, p2) _GL_UNSUPPORTED(glEvalMesh1)
+#define glEvalMesh2(mode, p1, p2, q1, q2) _GL_UNSUPPORTED(glEvalMesh2)
+#define glEvalPoint1(p) _GL_UNSUPPORTED(glEvalPoint1)
+#define glEvalPoint2(p, q) _GL_UNSUPPORTED(glEvalPoint2)
+
+/* Render mode */
+
+#define glRenderMode(mode) _GL_UNSUPPORTED(glRenderMode)
+
+/* Selection */
+
+#define glInitNames() _GL_UNSUPPORTED(glInitNames)
+#define glPopName() _GL_UNSUPPORTED(glPopName)
+#define glPushName(name) _GL_UNSUPPORTED(glPushName)
+#define glLoadName(name) _GL_UNSUPPORTED(glLoadName)
+#define glSelectBuffer(n, buffer) _GL_UNSUPPORTED(glSelectBuffer)
+
+/* Feedback */
+
+#define glFeedbackBuffer(n, type, buffer) _GL_UNSUPPORTED(glFeedbackBuffer)
+#define glPassThrough(token) _GL_UNSUPPORTED(glPassThrough)
+
+/* Display lists */
+
+void glNewList(GLuint n, GLenum mode);
+void glEndList(void);
+
+void glCallList(GLuint n);
+void glCallLists(GLsizei n, GLenum type, const GLvoid *lists);
+
+void glListBase(GLuint base);
+
+GLuint glGenLists(GLsizei s);
+
+GLboolean glIsList(GLuint list);
+
+void glDeleteLists(GLuint list, GLsizei range);
+
+/* Synchronization */
+
+void glFlush(void);
+void glFinish(void);
+
+/* Hints */
+
+void glHint(GLenum target, GLenum hint);
+
+/* Multisampling */
+
+#define glSampleCoverageARB(value, invert) _GL_UNSUPPORTED(glSampleCoverageARB)
+
+/* Queries */
+
+void glGetBooleanv(GLenum value, GLboolean *data);
+void glGetIntegerv(GLenum value, GLint *data);
+void glGetFloatv(GLenum value, GLfloat *data);
+void glGetDoublev(GLenum value, GLdouble *data);
+
+GLboolean glIsEnabled(GLenum value);
+
+void glGetPointerv(GLenum pname, GLvoid **params);
+
+GLubyte *glGetString(GLenum name);
+
+#define glGetClipPlane(plane, eqn) _GL_UNSUPPORTED(glGetClipPlane)
+#define glGetLightiv(light, value, data) _GL_UNSUPPORTED(glGetLightiv)
+#define glGetLightfv(light, value, data) _GL_UNSUPPORTED(glGetLightfv)
+#define glGetMaterialiv(face, value, data) _GL_UNSUPPORTED(glGetMaterialiv)
+#define glGetMaterialfv(face, value, data) _GL_UNSUPPORTED(glGetMaterialfv)
+#define glGetTexEnviv(env, value, data) _GL_UNSUPPORTED(glGetTexEnviv)
+#define glGetTexEnvfv(env, value, data) _GL_UNSUPPORTED(glGetTexEnvfv)
+#define glGetTexGeniv(coord, value, data) _GL_UNSUPPORTED(glGetTexGeniv)
+#define glGetTexGenfv(coord, value, data) _GL_UNSUPPORTED(glGetTexGenfv)
+#define glGetTexParameteriv(target, value, data) _GL_UNSUPPORTED(glGetTexParameteriv)
+#define glGetTexParameterfv(target, value, data) _GL_UNSUPPORTED(glGetTexParameterfv)
+#define glGetTexLevelParameteriv(target, lod, value, data) _GL_UNSUPPORTED(glGetTexLevelParameteriv)
+#define glGetTexLevelParameterfv(target, lod, value, data) _GL_UNSUPPORTED(glGetTexLevelParameterfv)
+#define glGetPixelMapusv(map, data) _GL_UNSUPPORTED(glGetPixelMapusv)
+#define glGetPixelMapuiv(map, data) _GL_UNSUPPORTED(glGetPixelMapuiv)
+#define glGetPixelMapfv(map, data) _GL_UNSUPPORTED(glGetPixelMapfv)
+#define glGetTexImage(tex, lod, format, type, img) _GL_UNSUPPORTED(glGetTexImage)
+#define glGetMapiv(map, value, data) _GL_UNSUPPORTED(glGetMapiv)
+#define glGetMapfv(map, value, data) _GL_UNSUPPORTED(glGetMapfv)
+#define glGetMapdv(map, value, data) _GL_UNSUPPORTED(glGetMapdv)
+#define glGetPolygonStipple(pattern) _GL_UNSUPPORTED(glGetPolygonStipple)
+
+/* Attribute stack */
+
+#define glPushAttrib(mask) _GL_UNSUPPORTED(glPushAttrib)
+#define glPushClientAttrib(mask) _GL_UNSUPPORTED(glPushClientAttrib)
+
+#define glPopAttrib() _GL_UNSUPPORTED(glPopAttrib)
+#define glPopClientAttrib() _GL_UNSUPPORTED(glPopClientAttrib)
+
+/* Deprecated functions (will be removed on trunk) */
+
+__attribute__((deprecated("use glSurfaceTexImageN64 instead")))
+inline void glTexImageN64(GLenum target, GLint level, surface_t *surface)
+{
+ glSurfaceTexImageN64(target, level, surface, NULL);
+}
+
+__attribute__((deprecated("use glSpriteTextureN64 instead")))
+inline void glTexSpriteN64(GLenum target, sprite_t *sprite)
+{
+ glSpriteTextureN64(target, sprite, NULL);
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+
+#endif
diff --git a/include/GL/gl_enums.h b/include/GL/gl_enums.h
new file mode 100644
index 000000000..191575e5f
--- /dev/null
+++ b/include/GL/gl_enums.h
@@ -0,0 +1,729 @@
+/**
+ * @file gl_enums.h
+ * @author Dennis Heinze
+ * @brief OpenGL enumeration and constant definitions for N64 implementation.
+ * @preview
+ */
+#ifndef __LIBDRAGON_GL_ENUMS_H
+#define __LIBDRAGON_GL_ENUMS_H
+
+// No LIBDRAGON_PREVIEW_HEADER here: this header is only reached through gl.h,
+// which already reports the error. Gating it as well would just report it twice.
+
+#define GL_BYTE 0x1400
+#define GL_UNSIGNED_BYTE 0x1401
+#define GL_SHORT 0x1402
+#define GL_UNSIGNED_SHORT 0x1403
+#define GL_INT 0x1404
+#define GL_UNSIGNED_INT 0x1405
+#define GL_FLOAT 0x1406
+#define GL_DOUBLE 0x140A
+#define GL_HALF_FIXED_N64 0x6F00
+
+#define GL_NO_ERROR 0
+#define GL_INVALID_ENUM 0x0500
+#define GL_INVALID_VALUE 0x0501
+#define GL_INVALID_OPERATION 0x0502
+#define GL_STACK_OVERFLOW 0x0503
+#define GL_STACK_UNDERFLOW 0x0504
+#define GL_OUT_OF_MEMORY 0x0505
+
+#define GL_DITHER 0x0BD0
+
+#define GL_POINTS 0x0000
+#define GL_LINES 0x0001
+#define GL_LINE_LOOP 0x0002
+#define GL_LINE_STRIP 0x0003
+#define GL_TRIANGLES 0x0004
+#define GL_TRIANGLE_STRIP 0x0005
+#define GL_TRIANGLE_FAN 0x0006
+#define GL_QUADS 0x0007
+#define GL_QUAD_STRIP 0x0008
+#define GL_POLYGON 0x0009
+
+#define GL_NORMALIZE 0x0BA1
+
+#define GL_CURRENT_COLOR 0x0B00
+#define GL_CURRENT_INDEX 0x0B01
+#define GL_CURRENT_NORMAL 0x0B02
+#define GL_CURRENT_TEXTURE_COORDS 0x0B03
+#define GL_CURRENT_RASTER_COLOR 0x0B04
+#define GL_CURRENT_RASTER_INDEX 0x0B05
+#define GL_CURRENT_RASTER_TEXTURE_COORDS 0x0B06
+#define GL_CURRENT_RASTER_POSITION 0x0B07
+#define GL_CURRENT_RASTER_POSITION_VALID 0x0B08
+#define GL_CURRENT_RASTER_DISTANCE 0x0B09
+
+#define GL_EDGE_FLAG 0x0B43
+
+#define GL_VERTEX_HALF_FIXED_PRECISION_N64 0x6F20
+#define GL_TEXTURE_COORD_HALF_FIXED_PRECISION_N64 0x6F21
+
+#define GL_VERTEX_ARRAY 0x8074
+#define GL_NORMAL_ARRAY 0x8075
+#define GL_COLOR_ARRAY 0x8076
+#define GL_INDEX_ARRAY 0x8077
+#define GL_TEXTURE_COORD_ARRAY 0x8078
+#define GL_EDGE_FLAG_ARRAY 0x8079
+
+#define GL_V2F 0x2A20
+#define GL_V3F 0x2A21
+#define GL_C4UB_V2F 0x2A22
+#define GL_C4UB_V3F 0x2A23
+#define GL_C3F_V3F 0x2A24
+#define GL_N3F_V3F 0x2A25
+#define GL_C4F_N3F_V3F 0x2A26
+#define GL_T2F_V3F 0x2A27
+#define GL_T4F_V4F 0x2A28
+#define GL_T2F_C4UB_V3F 0x2A29
+#define GL_T2F_C3F_V3F 0x2A2A
+#define GL_T2F_N3F_V3F 0x2A2B
+#define GL_T2F_C4F_N3F_V3F 0x2A2C
+#define GL_T4F_C4F_N3F_V4F 0x2A2D
+
+#define GL_VERTEX_ARRAY_SIZE 0x807A
+#define GL_VERTEX_ARRAY_TYPE 0x807B
+#define GL_VERTEX_ARRAY_STRIDE 0x807C
+
+#define GL_NORMAL_ARRAY_TYPE 0x807E
+#define GL_NORMAL_ARRAY_STRIDE 0x807F
+
+#define GL_COLOR_ARRAY_SIZE 0x8081
+#define GL_COLOR_ARRAY_TYPE 0x8082
+#define GL_COLOR_ARRAY_STRIDE 0x8083
+
+#define GL_INDEX_ARRAY_TYPE 0x8085
+#define GL_INDEX_ARRAY_STRIDE 0x8086
+
+#define GL_TEXTURE_COORD_ARRAY_SIZE 0x8088
+#define GL_TEXTURE_COORD_ARRAY_TYPE 0x8089
+#define GL_TEXTURE_COORD_ARRAY_STRIDE 0x808A
+
+#define GL_EDGE_FLAG_ARRAY_STRIDE 0x808C
+
+#define GL_VERTEX_ARRAY_POINTER 0x808E
+#define GL_NORMAL_ARRAY_POINTER 0x808F
+#define GL_COLOR_ARRAY_POINTER 0x8090
+#define GL_INDEX_ARRAY_POINTER 0x8091
+#define GL_TEXTURE_COORD_ARRAY_POINTER 0x8092
+#define GL_EDGE_FLAG_ARRAY_POINTER 0x8093
+
+#define GL_ARRAY_BUFFER_ARB 0x8892
+#define GL_ELEMENT_ARRAY_BUFFER_ARB 0x8893
+
+#define GL_ARRAY_BUFFER_BINDING_ARB 0x8894
+#define GL_ELEMENT_ARRAY_BUFFER_BINDING_ARB 0x8895
+#define GL_VERTEX_ARRAY_BUFFER_BINDING_ARB 0x8896
+#define GL_NORMAL_ARRAY_BUFFER_BINDING_ARB 0x8897
+#define GL_COLOR_ARRAY_BUFFER_BINDING_ARB 0x8898
+#define GL_INDEX_ARRAY_BUFFER_BINDING_ARB 0x8899
+#define GL_TEXTURE_COORD_ARRAY_BUFFER_BINDING_ARB 0x889A
+#define GL_EDGE_FLAG_ARRAY_BUFFER_BINDING_ARB 0x889B
+#define GL_MATRIX_INDEX_ARRAY_BUFFER_BINDING_ARB 0x889F
+
+#define GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING_ARB 0x889F
+
+#define GL_VERTEX_ARRAY_BINDING 0x85B5
+
+#define GL_MATRIX_PALETTE_ARB 0x8840
+#define GL_MAX_MATRIX_PALETTE_STACK_DEPTH_ARB 0x8841
+#define GL_MAX_PALETTE_MATRICES_ARB 0x8842
+#define GL_CURRENT_PALETTE_MATRIX_ARB 0x8843
+#define GL_MATRIX_INDEX_ARRAY_ARB 0x8844
+#define GL_CURRENT_MATRIX_INDEX_ARB 0x8845
+#define GL_MATRIX_INDEX_ARRAY_SIZE_ARB 0x8846
+#define GL_MATRIX_INDEX_ARRAY_TYPE_ARB 0x8847
+#define GL_MATRIX_INDEX_ARRAY_STRIDE_ARB 0x8848
+#define GL_MATRIX_INDEX_ARRAY_POINTER_ARB 0x8849
+
+#define GL_STREAM_DRAW_ARB 0x88E0
+#define GL_STREAM_READ_ARB 0x88E1
+#define GL_STREAM_COPY_ARB 0x88E2
+#define GL_STATIC_DRAW_ARB 0x88E4
+#define GL_STATIC_READ_ARB 0x88E5
+#define GL_STATIC_COPY_ARB 0x88E6
+#define GL_DYNAMIC_DRAW_ARB 0x88E8
+#define GL_DYNAMIC_READ_ARB 0x88E9
+#define GL_DYNAMIC_COPY_ARB 0x88EA
+
+#define GL_READ_ONLY_ARB 0x88B8
+#define GL_WRITE_ONLY_ARB 0x88B9
+#define GL_READ_WRITE_ARB 0x88BA
+
+#define GL_BUFFER_SIZE_ARB 0x8764
+#define GL_BUFFER_USAGE_ARB 0x8765
+#define GL_BUFFER_ACCESS_ARB 0x88BB
+#define GL_BUFFER_MAPPED_ARB 0x88BC
+
+#define GL_BUFFER_MAP_POINTER_ARB 0x88BD
+
+#define GL_DEPTH_RANGE 0x0B70
+#define GL_VIEWPORT 0x0BA2
+
+#define GL_MAX_VIEWPORT_DIMS 0x0D3A
+
+#define GL_MODELVIEW 0x1700
+#define GL_PROJECTION 0x1701
+#define GL_TEXTURE 0x1702
+
+#define GL_MATRIX_MODE 0x0BA0
+
+#define GL_MODELVIEW_STACK_DEPTH 0x0BA3
+#define GL_PROJECTION_STACK_DEPTH 0x0BA4
+#define GL_TEXTURE_STACK_DEPTH 0x0BA5
+
+#define GL_MODELVIEW_MATRIX 0x0BA6
+#define GL_PROJECTION_MATRIX 0x0BA7
+#define GL_TEXTURE_MATRIX 0x0BA8
+
+#define GL_MAX_MODELVIEW_STACK_DEPTH 0x0D36
+#define GL_MAX_PROJECTION_STACK_DEPTH 0x0D38
+#define GL_MAX_TEXTURE_STACK_DEPTH 0x0D39
+
+#define GL_TEXTURE_GEN_S 0x0C60
+#define GL_TEXTURE_GEN_T 0x0C61
+#define GL_TEXTURE_GEN_R 0x0C62
+#define GL_TEXTURE_GEN_Q 0x0C63
+
+#define GL_TEXTURE_GEN_MODE 0x2500
+#define GL_OBJECT_PLANE 0x2501
+#define GL_EYE_PLANE 0x2502
+
+#define GL_EYE_LINEAR 0x2400
+#define GL_OBJECT_LINEAR 0x2401
+#define GL_SPHERE_MAP 0x2402
+
+#define GL_CLIP_PLANE0 0x3000
+#define GL_CLIP_PLANE1 0x3001
+#define GL_CLIP_PLANE2 0x3002
+#define GL_CLIP_PLANE3 0x3003
+#define GL_CLIP_PLANE4 0x3004
+#define GL_CLIP_PLANE5 0x3005
+
+#define GL_MAX_CLIP_PLANES 0x0D32
+
+#define GL_LIGHTING 0x0B50
+#define GL_LIGHT_MODEL_LOCAL_VIEWER 0x0B51
+#define GL_LIGHT_MODEL_TWO_SIDE 0x0B52
+#define GL_LIGHT_MODEL_AMBIENT 0x0B53
+#define GL_SHADE_MODEL 0x0B54
+#define GL_COLOR_MATERIAL_FACE 0x0B55
+#define GL_COLOR_MATERIAL_PARAMETER 0x0B56
+#define GL_COLOR_MATERIAL 0x0B57
+
+#define GL_LIGHT0 0x4000
+#define GL_LIGHT1 0x4001
+#define GL_LIGHT2 0x4002
+#define GL_LIGHT3 0x4003
+#define GL_LIGHT4 0x4004
+#define GL_LIGHT5 0x4005
+#define GL_LIGHT6 0x4006
+#define GL_LIGHT7 0x4007
+
+#define GL_MAX_LIGHTS 0x0D31
+
+#define GL_AMBIENT 0x1200
+#define GL_DIFFUSE 0x1201
+#define GL_SPECULAR 0x1202
+#define GL_POSITION 0x1203
+#define GL_SPOT_DIRECTION 0x1204
+#define GL_SPOT_EXPONENT 0x1205
+#define GL_SPOT_CUTOFF 0x1206
+#define GL_CONSTANT_ATTENUATION 0x1207
+#define GL_LINEAR_ATTENUATION 0x1208
+#define GL_QUADRATIC_ATTENUATION 0x1209
+
+#define GL_EMISSION 0x1600
+#define GL_SHININESS 0x1601
+#define GL_AMBIENT_AND_DIFFUSE 0x1602
+#define GL_COLOR_INDEXES 0x1603
+
+#define GL_FLAT 0x1D00
+#define GL_SMOOTH 0x1D01
+
+#define GL_POINT_SMOOTH 0x0B10
+#define GL_POINT_SIZE 0x0B11
+#define GL_POINT_SIZE_GRANULARITY 0x0B12
+#define GL_POINT_SIZE_RANGE 0x0B13
+
+#define GL_LINE_SMOOTH 0x0B20
+#define GL_LINE_WIDTH 0x0B21
+#define GL_LINE_WIDTH_RANGE 0x0B22
+#define GL_LINE_WIDTH_GRANULARITY 0x0B23
+#define GL_LINE_STIPPLE 0x0B24
+#define GL_LINE_STIPPLE_PATTERN 0x0B25
+#define GL_LINE_STIPPLE_REPEAT 0x0B26
+
+#define GL_POLYGON_MODE 0x0B40
+#define GL_POLYGON_SMOOTH 0x0B41
+#define GL_POLYGON_STIPPLE 0x0B42
+#define GL_CULL_FACE 0x0B44
+#define GL_CULL_FACE_MODE 0x0B45
+#define GL_FRONT_FACE 0x0B46
+
+#define GL_CW 0x0900
+#define GL_CCW 0x0901
+
+#define GL_POINT 0x1B00
+#define GL_LINE 0x1B01
+#define GL_FILL 0x1B02
+
+#define GL_POLYGON_OFFSET_UNITS 0x2A00
+#define GL_POLYGON_OFFSET_POINT 0x2A01
+#define GL_POLYGON_OFFSET_LINE 0x2A02
+#define GL_POLYGON_OFFSET_FILL 0x8037
+#define GL_POLYGON_OFFSET_FACTOR 0x8038
+
+#define GL_UNPACK_SWAP_BYTES 0x0CF0
+#define GL_UNPACK_LSB_FIRST 0x0CF1
+#define GL_UNPACK_ROW_LENGTH 0x0CF2
+#define GL_UNPACK_SKIP_ROWS 0x0CF3
+#define GL_UNPACK_SKIP_PIXELS 0x0CF4
+#define GL_UNPACK_ALIGNMENT 0x0CF5
+
+#define GL_PACK_SWAP_BYTES 0x0D00
+#define GL_PACK_LSB_FIRST 0x0D01
+#define GL_PACK_ROW_LENGTH 0x0D02
+#define GL_PACK_SKIP_ROWS 0x0D03
+#define GL_PACK_SKIP_PIXELS 0x0D04
+#define GL_PACK_ALIGNMENT 0x0D05
+
+
+#define GL_MAP_COLOR 0x0D10
+#define GL_MAP_STENCIL 0x0D11
+#define GL_INDEX_SHIFT 0x0D12
+#define GL_INDEX_OFFSET 0x0D13
+#define GL_RED_SCALE 0x0D14
+#define GL_RED_BIAS 0x0D15
+#define GL_ZOOM_X 0x0D16
+#define GL_ZOOM_Y 0x0D17
+#define GL_GREEN_SCALE 0x0D18
+#define GL_GREEN_BIAS 0x0D19
+#define GL_BLUE_SCALE 0x0D1A
+#define GL_BLUE_BIAS 0x0D1B
+#define GL_ALPHA_SCALE 0x0D1C
+#define GL_ALPHA_BIAS 0x0D1D
+#define GL_DEPTH_SCALE 0x0D1E
+#define GL_DEPTH_BIAS 0x0D1F
+
+#define GL_PIXEL_MAP_I_TO_I 0x0C70
+#define GL_PIXEL_MAP_S_TO_S 0x0C71
+#define GL_PIXEL_MAP_I_TO_R 0x0C72
+#define GL_PIXEL_MAP_I_TO_G 0x0C73
+#define GL_PIXEL_MAP_I_TO_B 0x0C74
+#define GL_PIXEL_MAP_I_TO_A 0x0C75
+#define GL_PIXEL_MAP_R_TO_R 0x0C76
+#define GL_PIXEL_MAP_G_TO_G 0x0C77
+#define GL_PIXEL_MAP_B_TO_B 0x0C78
+#define GL_PIXEL_MAP_A_TO_A 0x0C79
+
+#define GL_COLOR 0x1800
+#define GL_DEPTH 0x1801
+#define GL_STENCIL 0x1802
+
+#define GL_READ_BUFFER 0x0C02
+
+#define GL_PIXEL_MAP_I_TO_I_SIZE 0x0CB0
+#define GL_PIXEL_MAP_S_TO_S_SIZE 0x0CB1
+#define GL_PIXEL_MAP_I_TO_R_SIZE 0x0CB2
+#define GL_PIXEL_MAP_I_TO_G_SIZE 0x0CB3
+#define GL_PIXEL_MAP_I_TO_B_SIZE 0x0CB4
+#define GL_PIXEL_MAP_I_TO_A_SIZE 0x0CB5
+#define GL_PIXEL_MAP_R_TO_R_SIZE 0x0CB6
+#define GL_PIXEL_MAP_G_TO_G_SIZE 0x0CB7
+#define GL_PIXEL_MAP_B_TO_B_SIZE 0x0CB8
+#define GL_PIXEL_MAP_A_TO_A_SIZE 0x0CB9
+
+#define GL_MAX_PIXEL_MAP_TABLE 0x0D34
+
+#define GL_BITMAP 0x1A00
+
+#define GL_COLOR_INDEX 0x1900
+#define GL_STENCIL_INDEX 0x1901
+#define GL_DEPTH_COMPONENT 0x1902
+#define GL_RED 0x1903
+#define GL_GREEN 0x1904
+#define GL_BLUE 0x1905
+#define GL_ALPHA 0x1906
+#define GL_RGB 0x1907
+#define GL_RGBA 0x1908
+#define GL_LUMINANCE 0x1909
+#define GL_LUMINANCE_ALPHA 0x190A
+
+#define GL_R3_G3_B2 0x2A10
+#define GL_ALPHA4 0x803B
+#define GL_ALPHA8 0x803C
+#define GL_ALPHA12 0x803D
+#define GL_ALPHA16 0x803E
+#define GL_LUMINANCE4 0x803F
+#define GL_LUMINANCE8 0x8040
+#define GL_LUMINANCE12 0x8041
+#define GL_LUMINANCE16 0x8042
+#define GL_LUMINANCE4_ALPHA4 0x8043
+#define GL_LUMINANCE6_ALPHA2 0x8044
+#define GL_LUMINANCE8_ALPHA8 0x8045
+#define GL_LUMINANCE12_ALPHA4 0x8046
+#define GL_LUMINANCE12_ALPHA12 0x8047
+#define GL_LUMINANCE16_ALPHA16 0x8048
+#define GL_INTENSITY 0x8049
+#define GL_INTENSITY4 0x804A
+#define GL_INTENSITY8 0x804B
+#define GL_INTENSITY12 0x804C
+#define GL_INTENSITY16 0x804D
+#define GL_RGB4 0x804F
+#define GL_RGB5 0x8050
+#define GL_RGB8 0x8051
+#define GL_RGB10 0x8052
+#define GL_RGB12 0x8053
+#define GL_RGB16 0x8054
+#define GL_RGBA2 0x8055
+#define GL_RGBA4 0x8056
+#define GL_RGB5_A1 0x8057
+#define GL_RGBA8 0x8058
+#define GL_RGB10_A2 0x8059
+#define GL_RGBA12 0x805A
+#define GL_RGBA16 0x805B
+
+#define GL_UNSIGNED_BYTE_3_3_2_EXT 0x8032
+#define GL_UNSIGNED_SHORT_4_4_4_4_EXT 0x8033
+#define GL_UNSIGNED_SHORT_5_5_5_1_EXT 0x8034
+#define GL_UNSIGNED_INT_8_8_8_8_EXT 0x8035
+#define GL_UNSIGNED_INT_10_10_10_2_EXT 0x8036
+
+#define GL_TEXTURE_1D 0x0DE0
+#define GL_TEXTURE_2D 0x0DE1
+#define GL_PROXY_TEXTURE_1D 0x8063
+#define GL_PROXY_TEXTURE_2D 0x8064
+
+#define GL_TEXTURE_MAG_FILTER 0x2800
+#define GL_TEXTURE_MIN_FILTER 0x2801
+#define GL_TEXTURE_WRAP_S 0x2802
+#define GL_TEXTURE_WRAP_T 0x2803
+#define GL_TEXTURE_WIDTH 0x1000
+#define GL_TEXTURE_HEIGHT 0x1001
+#define GL_TEXTURE_INTERNAL_FORMAT 0x1003
+#define GL_TEXTURE_BORDER_COLOR 0x1004
+#define GL_TEXTURE_BORDER 0x1005
+#define GL_TEXTURE_RED_SIZE 0x805C
+#define GL_TEXTURE_GREEN_SIZE 0x805D
+#define GL_TEXTURE_BLUE_SIZE 0x805E
+#define GL_TEXTURE_ALPHA_SIZE 0x805F
+#define GL_TEXTURE_LUMINANCE_SIZE 0x8060
+#define GL_TEXTURE_INTENSITY_SIZE 0x8061
+#define GL_TEXTURE_PRIORITY 0x8066
+#define GL_TEXTURE_RESIDENT 0x8067
+
+#define GL_NEAREST 0x2600
+#define GL_LINEAR 0x2601
+#define GL_NEAREST_MIPMAP_NEAREST 0x2700
+#define GL_LINEAR_MIPMAP_NEAREST 0x2701
+#define GL_NEAREST_MIPMAP_LINEAR 0x2702
+#define GL_LINEAR_MIPMAP_LINEAR 0x2703
+
+#define GL_CLAMP 0x2900
+#define GL_REPEAT 0x2901
+#define GL_MIRRORED_REPEAT_ARB 0x8370
+
+#define GL_TEXTURE_ENV 0x2300
+#define GL_TEXTURE_ENV_MODE 0x2200
+#define GL_TEXTURE_ENV_COLOR 0x2201
+#define GL_MODULATE 0x2100
+#define GL_DECAL 0x2101
+#define GL_BLEND 0x0BE2
+#define GL_REPLACE 0x1E01
+
+#define GL_S 0x2000
+#define GL_T 0x2001
+#define GL_R 0x2002
+#define GL_Q 0x2003
+
+#define GL_MAX_TEXTURE_SIZE 0x0D33
+
+#define GL_TEXTURE_FLIP_T_N64 0x6020
+
+#define GL_FOG 0x0B60
+#define GL_FOG_INDEX 0x0B61
+#define GL_FOG_DENSITY 0x0B62
+#define GL_FOG_START 0x0B63
+#define GL_FOG_END 0x0B64
+#define GL_FOG_MODE 0x0B65
+#define GL_FOG_COLOR 0x0B66
+
+#define GL_EXP 0x0800
+#define GL_EXP2 0x0801
+
+#define GL_SCISSOR_BOX 0x0C10
+#define GL_SCISSOR_TEST 0x0C11
+
+#define GL_ALPHA_TEST 0x0BC0
+#define GL_ALPHA_TEST_FUNC 0x0BC1
+#define GL_ALPHA_TEST_REF 0x0BC2
+
+#define GL_NEVER 0x0200
+#define GL_LESS 0x0201
+#define GL_EQUAL 0x0202
+#define GL_LEQUAL 0x0203
+#define GL_GREATER 0x0204
+#define GL_NOTEQUAL 0x0205
+#define GL_GEQUAL 0x0206
+#define GL_ALWAYS 0x0207
+#define GL_LESS_INTERPENETRATING_N64 0x6010
+
+#define GL_STENCIL_TEST 0x0B90
+#define GL_STENCIL_FUNC 0x0B92
+#define GL_STENCIL_VALUE_MASK 0x0B93
+#define GL_STENCIL_FAIL 0x0B94
+#define GL_STENCIL_PASS_DEPTH_FAIL 0x0B95
+#define GL_STENCIL_PASS_DEPTH_PASS 0x0B96
+#define GL_STENCIL_REF 0x0B97
+
+#define GL_KEEP 0x1E00
+#define GL_INCR 0x1E02
+#define GL_DECR 0x1E03
+
+#define GL_DEPTH_TEST 0x0B71
+#define GL_DEPTH_FUNC 0x0B74
+
+#define GL_BLEND_DST 0x0BE0
+#define GL_BLEND_SRC 0x0BE1
+#define GL_BLEND 0x0BE2
+
+#define GL_ZERO 0
+#define GL_ONE 1
+#define GL_SRC_COLOR 0x0300
+#define GL_ONE_MINUS_SRC_COLOR 0x0301
+#define GL_SRC_ALPHA 0x0302
+#define GL_ONE_MINUS_SRC_ALPHA 0x0303
+#define GL_DST_COLOR 0x0304
+#define GL_ONE_MINUS_DST_COLOR 0x0305
+#define GL_DST_ALPHA 0x0306
+#define GL_ONE_MINUS_DST_ALPHA 0x0307
+#define GL_SRC_ALPHA_SATURATE 0x0308
+
+#define GL_CLEAR 0x1500
+#define GL_AND 0x1501
+#define GL_AND_REVERSE 0x1502
+#define GL_COPY 0x1503
+#define GL_AND_INVERTED 0x1504
+#define GL_NOOP 0x1505
+#define GL_XOR 0x1506
+#define GL_OR 0x1507
+#define GL_NOR 0x1508
+#define GL_EQUIV 0x1509
+#define GL_INVERT 0x150A
+#define GL_OR_REVERSE 0x150B
+#define GL_COPY_INVERTED 0x150C
+#define GL_OR_INVERTED 0x150D
+#define GL_NAND 0x150E
+#define GL_SET 0x150F
+
+#define GL_LOGIC_OP_MODE 0x0BF0
+#define GL_INDEX_LOGIC_OP 0x0BF1
+#define GL_LOGIC_OP 0x0BF1
+#define GL_COLOR_LOGIC_OP 0x0BF3
+
+#define GL_NONE 0
+#define GL_FRONT_LEFT 0x0400
+#define GL_FRONT_RIGHT 0x0401
+#define GL_BACK_LEFT 0x0402
+#define GL_BACK_RIGHT 0x0403
+#define GL_FRONT 0x0404
+#define GL_BACK 0x0405
+#define GL_LEFT 0x0406
+#define GL_RIGHT 0x0407
+#define GL_FRONT_AND_BACK 0x0408
+#define GL_AUX0 0x0409
+#define GL_AUX1 0x040A
+#define GL_AUX2 0x040B
+#define GL_AUX3 0x040C
+
+#define GL_AUX_BUFFERS 0x0C00
+#define GL_DRAW_BUFFER 0x0C01
+
+#define GL_INDEX_WRITEMASK 0x0C21
+#define GL_COLOR_WRITEMASK 0x0C23
+#define GL_DEPTH_WRITEMASK 0x0B72
+#define GL_STENCIL_WRITEMASK 0x0B98
+
+#define GL_DEPTH_BUFFER_BIT 0x00000100
+#define GL_ACCUM_BUFFER_BIT 0x00000200
+#define GL_STENCIL_BUFFER_BIT 0x00000400
+#define GL_COLOR_BUFFER_BIT 0x00004000
+
+#define GL_COLOR_CLEAR_VALUE 0x0C22
+#define GL_DEPTH_CLEAR_VALUE 0x0B73
+#define GL_INDEX_CLEAR_VALUE 0x0C20
+#define GL_STENCIL_CLEAR_VALUE 0x0B91
+#define GL_ACCUM_CLEAR_VALUE 0x0B80
+
+#define GL_ACCUM 0x0100
+#define GL_LOAD 0x0101
+#define GL_RETURN 0x0102
+#define GL_MULT 0x0103
+#define GL_ADD 0x0104
+
+#define GL_ACCUM_RED_BITS 0x0D58
+#define GL_ACCUM_GREEN_BITS 0x0D59
+#define GL_ACCUM_BLUE_BITS 0x0D5A
+#define GL_ACCUM_ALPHA_BITS 0x0D5B
+
+#define GL_AUTO_NORMAL 0x0D80
+
+#define GL_MAP1_COLOR_4 0x0D90
+#define GL_MAP1_INDEX 0x0D91
+#define GL_MAP1_NORMAL 0x0D92
+#define GL_MAP1_TEXTURE_COORD_1 0x0D93
+#define GL_MAP1_TEXTURE_COORD_2 0x0D94
+#define GL_MAP1_TEXTURE_COORD_3 0x0D95
+#define GL_MAP1_TEXTURE_COORD_4 0x0D96
+#define GL_MAP1_VERTEX_3 0x0D97
+#define GL_MAP1_VERTEX_4 0x0D98
+
+#define GL_MAP2_COLOR_4 0x0DB0
+#define GL_MAP2_INDEX 0x0DB1
+#define GL_MAP2_NORMAL 0x0DB2
+#define GL_MAP2_TEXTURE_COORD_1 0x0DB3
+#define GL_MAP2_TEXTURE_COORD_2 0x0DB4
+#define GL_MAP2_TEXTURE_COORD_3 0x0DB5
+#define GL_MAP2_TEXTURE_COORD_4 0x0DB6
+#define GL_MAP2_VERTEX_3 0x0DB7
+#define GL_MAP2_VERTEX_4 0x0DB8
+
+#define GL_MAP1_GRID_DOMAIN 0x0DD0
+#define GL_MAP1_GRID_SEGMENTS 0x0DD1
+#define GL_MAP2_GRID_DOMAIN 0x0DD2
+#define GL_MAP2_GRID_SEGMENTS 0x0DD3
+
+#define GL_MAX_EVAL_ORDER 0x0D30
+
+#define GL_RENDER 0x1C00
+#define GL_FEEDBACK 0x1C01
+#define GL_SELECT 0x1C02
+
+#define GL_RENDER_MODE 0x0C40
+
+#define GL_SELECTION_BUFFER_POINTER 0x0DF3
+#define GL_NAME_STACK_DEPTH 0x0D70
+#define GL_MAX_NAME_STACK_DEPTH 0x0D37
+
+#define GL_2D 0x0600
+#define GL_3D 0x0601
+#define GL_3D_COLOR 0x0602
+#define GL_3D_COLOR_TEXTURE 0x0603
+#define GL_4D_COLOR_TEXTURE 0x0604
+
+#define GL_PASS_THROUGH_TOKEN 0x0700
+#define GL_POINT_TOKEN 0x0701
+#define GL_LINE_TOKEN 0x0702
+#define GL_POLYGON_TOKEN 0x0703
+#define GL_BITMAP_TOKEN 0x0704
+#define GL_DRAW_PIXEL_TOKEN 0x0705
+#define GL_COPY_PIXEL_TOKEN 0x0706
+#define GL_LINE_RESET_TOKEN 0x0707
+
+#define GL_FEEDBACK_BUFFER_POINTER 0x0DF0
+
+#define GL_COMPILE 0x1300
+#define GL_COMPILE_AND_EXECUTE 0x1301
+
+#define GL_2_BYTES 0x1407
+#define GL_3_BYTES 0x1408
+#define GL_4_BYTES 0x1409
+
+#define GL_LIST_MODE 0x0B30
+#define GL_MAX_LIST_NESTING 0x0B31
+#define GL_LIST_BASE 0x0B32
+#define GL_LIST_INDEX 0x0B33
+
+#define GL_PERSPECTIVE_CORRECTION_HINT 0x0C50
+#define GL_POINT_SMOOTH_HINT 0x0C51
+#define GL_LINE_SMOOTH_HINT 0x0C52
+#define GL_POLYGON_SMOOTH_HINT 0x0C53
+#define GL_FOG_HINT 0x0C54
+#define GL_MULTISAMPLE_HINT_N64 0x6000
+
+#define GL_DONT_CARE 0x1100
+#define GL_FASTEST 0x1101
+#define GL_NICEST 0x1102
+
+#define GL_MULTISAMPLE_ARB 0x809D
+#define GL_SAMPLE_ALPHA_TO_COVERAGE_ARB 0x809E
+#define GL_SAMPLE_ALPHA_TO_ONE_ARB 0x809F
+#define GL_SAMPLE_COVERAGE_ARB 0x80A0
+#define GL_SAMPLE_BUFFERS_ARB 0x80A8
+#define GL_SAMPLES_ARB 0x80A9
+#define GL_SAMPLE_COVERAGE_VALUE_ARB 0x80AA
+#define GL_SAMPLE_COVERAGE_INVERT_ARB 0x80AB
+
+#define GL_SUBPIXEL_BITS 0x0D50
+#define GL_INDEX_BITS 0x0D51
+#define GL_RED_BITS 0x0D52
+#define GL_GREEN_BITS 0x0D53
+#define GL_BLUE_BITS 0x0D54
+#define GL_ALPHA_BITS 0x0D55
+#define GL_DEPTH_BITS 0x0D56
+#define GL_STENCIL_BITS 0x0D57
+
+#define GL_COEFF 0x0A00
+#define GL_ORDER 0x0A01
+#define GL_DOMAIN 0x0A02
+
+#define GL_INDEX_MODE 0x0C30
+#define GL_RGBA_MODE 0x0C31
+#define GL_DOUBLEBUFFER 0x0C32
+#define GL_STEREO 0x0C33
+
+#define GL_VENDOR 0x1F00
+#define GL_RENDERER 0x1F01
+#define GL_VERSION 0x1F02
+#define GL_EXTENSIONS 0x1F03
+
+/** @brief Enum to enable usage of custom RDPQ materials with GL
+ *
+ * The GL_RDPQ_MATERIAL_N64 enum allows to setup a custom Color Combiner and/or Color Blender
+ * via rdpq_mode_combiner/rdpq_mode_blender and also allows to setup the RDP's internal color registers
+ * such as PRIM/ENV/BLEND etc. Disable it manually once usage has been fulfilled.
+ * Notice that even when this enum is enabled, GL calls and states are still processed as usual
+ * and will be used once the enum is disabled. */
+#define GL_RDPQ_MATERIAL_N64 0x6D10
+
+/** @brief Enum to enable loading and managing textures with RDPQ
+ *
+ * This enum allows to load custom textures and surfaces (for example with rdpq_tex_load/sub),
+ * allows to configure the tile and tilesize parameters of each tile,
+ * allows to setup custom mipmapping and to override rdpq_mode_filter filtering.
+ * Disable it manually once usage has been fulfilled.
+ * Notice that even when this enum is enabled, GL calls and states are still processed as usual
+ * and will be used once the enum is disabled. */
+#define GL_RDPQ_TEXTURING_N64 0x6D11
+
+#define GL_CURRENT_BIT 0x00000001
+#define GL_POINT_BIT 0x00000002
+#define GL_LINE_BIT 0x00000004
+#define GL_POLYGON_BIT 0x00000008
+#define GL_POLYGON_STIPPLE_BIT 0x00000010
+#define GL_PIXEL_MODE_BIT 0x00000020
+#define GL_LIGHTING_BIT 0x00000040
+#define GL_FOG_BIT 0x00000080
+#define GL_VIEWPORT_BIT 0x00000800
+#define GL_TRANSFORM_BIT 0x00001000
+#define GL_ENABLE_BIT 0x00002000
+#define GL_HINT_BIT 0x00008000
+#define GL_EVAL_BIT 0x00010000
+#define GL_LIST_BIT 0x00020000
+#define GL_TEXTURE_BIT 0x00040000
+#define GL_SCISSOR_BIT 0x00080000
+#define GL_MULTISAMPLE_BIT_ARB 0x20000000
+#define GL_ALL_ATTRIB_BITS 0xFFFFFFFF
+
+#define GL_CLIENT_PIXEL_STORE_BIT 0x00000001
+#define GL_CLIENT_VERTEX_ARRAY_BIT 0x00000002
+#define GL_CLIENT_ALL_ATTRIB_BITS 0xFFFFFFFF
+
+#define GL_ATTRIB_STACK_DEPTH 0x0BB0
+#define GL_CLIENT_ATTRIB_STACK_DEPTH 0x0BB1
+
+#define GL_MAX_ATTRIB_STACK_DEPTH 0x0D35
+#define GL_MAX_CLIENT_ATTRIB_STACK_DEPTH 0x0D3B
+
+#endif
diff --git a/include/GL/gl_integration.h b/include/GL/gl_integration.h
new file mode 100644
index 000000000..d5771b8bf
--- /dev/null
+++ b/include/GL/gl_integration.h
@@ -0,0 +1,28 @@
+/**
+ * @file gl_integration.h
+ * @author Dennis Heinze
+ * @author Giovanni Bajo
+ * @brief Integration hooks for OpenGL context and lifecycle management.
+ * @preview
+ */
+#ifndef __LIBDRAGON_GL_INTEGRATION
+#define __LIBDRAGON_GL_INTEGRATION
+
+#include "preview.h"
+LIBDRAGON_PREVIEW_HEADER
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+void gl_init(void);
+void gl_close(void);
+
+void gl_context_begin(void);
+void gl_context_end(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
\ No newline at end of file
diff --git a/include/GL/glu.h b/include/GL/glu.h
new file mode 100644
index 000000000..a90d6e6c3
--- /dev/null
+++ b/include/GL/glu.h
@@ -0,0 +1,27 @@
+/**
+ * @file glu.h
+ * @author Giovanni Bajo
+ * @brief OpenGL Utility Library (GLU) function declarations for N64.
+ * @preview
+ */
+#ifndef __LIBDRAGON_GLU_H
+#define __LIBDRAGON_GLU_H
+
+#include "preview.h"
+LIBDRAGON_PREVIEW_HEADER
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+void gluLookAt(float eyex, float eyey, float eyez,
+ float centerx, float centery, float centerz,
+ float upx, float upy, float upz);
+
+void gluPerspective(float fovy, float aspect, float zNear, float zFar);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/a3d.h b/include/a3d.h
new file mode 100644
index 000000000..e4c5d6b3a
--- /dev/null
+++ b/include/a3d.h
@@ -0,0 +1,21 @@
+/**
+ * @file a3d.h
+ * @author Giovanni Bajo
+ * @brief Analogue3D support
+ */
+
+ #ifndef LIBDRAGON_A3D_H
+ #define LIBDRAGON_A3D_H
+
+ #include
+ #include "preview.h"
+
+ /**
+ * @brief Detect if the running system is an Analogue3D
+ * @preview
+ */
+ LIBDRAGON_PREVIEW_API
+ bool a3d_detect(void);
+
+ #endif
+
\ No newline at end of file
diff --git a/include/asset.h b/include/asset.h
index 6e3847ab5..d91f2b76b 100644
--- a/include/asset.h
+++ b/include/asset.h
@@ -1,12 +1,14 @@
/**
* @file asset.h
* @author Giovanni Bajo
+ * @author Liam Coleman
* @brief Asset Subsystem
* @ingroup asset
*/
#ifndef __LIBDRAGON_ASSET_H
#define __LIBDRAGON_ASSET_H
+
/**
* @defgroup asset Asset Subsystem
* @ingroup libdragon
@@ -67,10 +69,8 @@
*/
#include
-
-#ifdef N64
+#include "preview.h"
#include "debug.h"
-#endif
#ifdef __cplusplus
extern "C" {
@@ -81,6 +81,10 @@ extern void __asset_init_compression_lvl2(void);
extern void __asset_init_compression_lvl3(void);
/// @endcond
+/** @brief Minimum required alignment for assets */
+#define ASSET_ALIGNMENT_MIN 16 ///< Aligned to data cacheline
+
+
/**
* @brief Enable a non-default compression level
*
@@ -122,10 +126,86 @@ extern void __asset_init_compression_lvl3(void);
*
* @param fn Filename to load (including filesystem prefix, eg: "rom:/foo.dat")
* @param sz If not NULL, this will be filed with the uncompressed size of the loaded file
- * @return void* Pointer to the loaded file (must be freed with free() when done)
+ * @return Pointer to the loaded file (must be freed with free() when done)
*/
void *asset_load(const char *fn, int *sz);
+/**
+ * @brief Load an asset file (possibly uncompressing it)
+ * @preview
+ *
+ * This function loads an asset embedded within a larger file. It requires in
+ * input an open file pointer, seeked to the beginning of the asset, and the
+ * size of the asset itself. If the asset is compressed, it is transparently
+ * decompressed.
+ *
+ * After this function returns, for technical reasons, the position of the
+ * provided file pointer becomes undefined. If you need to use it again, make
+ * sure to seek it.
+ *
+ * A memory buffer to hold the uncompressed asset is automatically allocated
+ * and returned. It must be freed using free() when the buffer is not required
+ * anymore. The memory is guaranteed to be aligned by at least #ASSET_ALIGNMENT_MIN.
+ *
+ * @param f pre-seeked file pointer, pointing to a valid asset header (or
+ * actual data if uncompressed)
+ * @param sz size of input data (compressed or not). It will be filled
+ * the uncompressed asset size, which is equal to the input value if the
+ * asset is not compressed.
+ * @return Allocated buffer filled with the uncompressed asset content
+ */
+LIBDRAGON_PREVIEW_API
+void* asset_loadf(FILE *f, int *sz);
+
+
+/**
+ * @brief Load an asset file (possibly uncompressing it)
+ * @preview
+ *
+ * This is the lowest-level asset loading function, that is
+ * needed only for advanced use cases. In general, prefer using
+ * any of other variants if possible, as the other APIs are
+ * harder to misuse.
+ *
+ * This function loads an asset potentially embedded within a
+ * larger, opened file. It requires an open file pointer, seeked
+ * to the beginning of the asset, and the size of the asset itself.
+ * If the asset is compressed, it is transparently decompressed.
+ *
+ * After this function returns, for technical reasons, the position
+ * of the provided file pointer becomes undefined. If you need to
+ * use it again, make sure to seek it.
+ *
+ * The memory buffer to hold the uncompressed asset must be provided as
+ * input, together with its size. If the provided buffer is too small (or
+ * it is NULL), the function does not load the asset and returns false,
+ * change buf_size to contain the minimum required size for the buffer.
+ * Notice that the minimum buffer size might be slightly larger than
+ * the uncompressed asset size, because some extra space might be required
+ * to perform in-place decompression. The minimum buffer size can either
+ * be calculated a build time (the assetcomp library exposes a function to
+ * do so), or queried at runtime by simply calling this function with a NULL
+ * input buffer.
+ *
+ * @param f pre-seeked file pointer, pointing to a valid asset header
+ * (or actual data if uncompressed)
+ * @param sz [in/out]: size of input data (compressed or not). It will
+ * be filled the uncompressed asset size, which is equal to
+ * the input value if the asset is not compressed.
+ * @param buf Pointer to the buffer where data must be loaded into.
+ * If the buffer pointer is NULL, or it is too small,
+ * asset_loadf_into will fail.
+ * @param buf_size [in/out]: Size of the provided input buffer. Changed to
+ * minimum required size, if it was too small.
+ *
+ * @return true The function has succeeded and the asset was loaded
+ * @return false The function has failed because the provided buffer was too small.
+ * In this case, *buf_size is changed to contain the minimum size
+ * that is required to load this asset.
+ */
+LIBDRAGON_PREVIEW_API
+bool asset_loadf_into(FILE *f, int *sz, void *buf, int *buf_size);
+
/**
* @brief Open an asset file for reading (with transparent decompression)
*
@@ -146,7 +226,7 @@ void *asset_load(const char *fn, int *sz);
*
* @param fn Filename to load (including filesystem prefix, eg: "rom:/foo.dat")
* @param sz If not NULL, this will be filed with the uncompressed size of the loaded file
- * @return FILE* FILE pointer to use with standard C functions (fread, fclose)
+ * @return FILE pointer to use with standard C functions (fread, fclose)
*/
FILE *asset_fopen(const char *fn, int *sz);
diff --git a/include/audio.h b/include/audio.h
index 896e8fb2e..70da011f1 100644
--- a/include/audio.h
+++ b/include/audio.h
@@ -1,6 +1,7 @@
/**
* @file audio.h
* @author Jennifer Taylor
+ * @author Giovanni Bajo
* @author thekovic
* @brief Audio Subsystem
* @ingroup audio
@@ -8,8 +9,10 @@
#ifndef __LIBDRAGON_AUDIO_H
#define __LIBDRAGON_AUDIO_H
+
#include
#include
+#include "preview.h"
/**
* @defgroup audio Audio Subsystem
@@ -31,12 +34,11 @@
* audio subsystem based on settings left by the bootloader.
*
* Code attempting to output audio on the N64 should initialize the
- * audio subsystem at the desired frequency and with the desired number
- * of buffers using #audio_init. More audio buffers allows for smaller
- * chances of audio glitches but means that there will be more latency
- * in sound output. When new data is available to be output, code should
- * check to see if there is room in the output buffers using
- * #audio_can_write. Code can probe the current frequency and buffer
+ * audio subsystem at the desired frequency and with the desired audio
+ * headroom using #audio_init. More headroom reduces the chance of audio
+ * glitches but increases output latency. When new data is available to be
+ * output, code should check to see if there is room in the output buffers
+ * using #audio_can_write. Code can probe the current frequency and buffer
* size using #audio_get_frequency and #audio_get_buffer_length respectively.
* When there is additional room, code can add new data to the output
* buffers using #audio_write. Be careful as this is a blocking operation,
@@ -62,21 +64,48 @@ extern "C" {
*/
typedef void(*audio_fill_buffer_callback)(short *buffer, size_t numsamples);
+/** @brief Convert a headroom in milliseconds to a #audio_init latency argument */
+#define AUDIO_INIT_LATENCY_MS(ms) ((ms) * 25 / 1000.0f)
+
+/** @brief Default #audio_init latency argument (~160 ms) */
+#define AUDIO_DEFAULT_LATENCY AUDIO_INIT_LATENCY_MS(160)
+
/**
* @brief Initialize the audio subsystem
*
* This function will set up the AI to play at a given frequency and
- * allocate a number of back buffers to write data to.
+ * allocate enough internal buffers to hold the requested amount of audio
+ * headroom. The latency argument uses a weird scale for backward compatibility.
+ * Please use #AUDIO_INIT_LATENCY_MS to specify a latency in milliseconds.
*
* @note Before re-initializing the audio subsystem to a new playback
* frequency, remember to call #audio_close.
*
* @param[in] frequency
* The frequency in Hz to play back samples at
- * @param[in] numbuffers
- * The number of buffers to allocate internally
+ * @param[in] latency
+ * Desired audio headroom. Use #AUDIO_INIT_LATENCY_MS to specify a
+ * latency in milliseconds. Use #AUDIO_DEFAULT_LATENCY for a good
+ * default.
+ */
+void audio_init(const int frequency, float latency);
+
+/**
+ * @brief Require AI buffer lengths to be a multiple of @p nsamples
+ * @preview
+ *
+ * After this call, #audio_get_buffer_length returns a multiple of @p nsamples,
+ * chosen closest to the default size for the current frequency. The audio
+ * headroom configured in #audio_init is preserved.
+ *
+ * Must be called while no buffers are queued for playback (typically right
+ * after #audio_init). #mixer_init calls this automatically.
+ *
+ * @param[in] nsamples
+ * Granularity in stereo samples; must be a positive multiple of 16
*/
-void audio_init(const int frequency, int numbuffers);
+LIBDRAGON_PREVIEW_API
+void audio_set_buffer_granularity(int nsamples);
/**
* @brief Install a audio callback to fill the audio buffer when required.
@@ -106,7 +135,7 @@ void audio_pause(bool pause);
* write data to. If all buffers are full, wait until the AI has played back
* the next buffer in its queue and try writing again.
*/
-volatile int audio_can_write();
+int audio_can_write();
/**
* @brief Write a chunk of silence
@@ -145,6 +174,31 @@ int audio_get_frequency();
*/
int audio_get_buffer_length();
+/**
+ * @brief Return the number of internal audio buffers
+ * @preview
+ *
+ * @return Number of buffers configured by #audio_init, or zero if the
+ * audio subsystem is not initialized
+ */
+LIBDRAGON_PREVIEW_API
+int audio_get_num_buffers(void);
+
+/**
+ * @brief Return the number of audio buffers currently containing data
+ * @preview
+ *
+ * The count includes buffers already submitted to AI and buffers waiting
+ * to be submitted. The value is a snapshot and may change asynchronously
+ * as the AI consumes buffers; any actual write must still be guarded by
+ * #audio_can_write.
+ *
+ * @return Number of currently full buffers, or zero if the audio subsystem
+ * is not initialized
+ */
+LIBDRAGON_PREVIEW_API
+int audio_get_queued_buffers(void);
+
/**
* @brief Start writing to the first free internal buffer.
@@ -202,7 +256,7 @@ void audio_write_end(void);
* @param buffer Buffer containing stereo samples to be played
* @param nsamples Number of stereo samples in the buffer
* @param blocking If true, wait until all samples have been pushed
- * @return int Number of samples pushed into output
+ * @return Number of samples pushed into output
*/
int audio_push(const short *buffer, int nsamples, bool blocking);
diff --git a/include/bb_rtc.h b/include/bb_rtc.h
new file mode 100644
index 000000000..5e4680189
--- /dev/null
+++ b/include/bb_rtc.h
@@ -0,0 +1,64 @@
+/**
+ * @file bb_rtc.h
+ * @author Christopher Bonhage
+ */
+#ifndef LIBDRAGON_BB_RTC_H
+#define LIBDRAGON_BB_RTC_H
+
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** @brief BBPlayer RTC minimum timestamp (2000-01-01 00:00:00) */
+#define BB_RTC_TIMESTAMP_MIN 946684800
+/**
+ * @brief BBPlayer RTC maximum timestamp (2099-12-31 23:59:59)
+ *
+ * The RTC has a "century" bit that could be used to extend the
+ * date range, but the iQue Menu disables it. LibDragon currently
+ * will respect the century bit if it is enabled, but will not
+ * automatically enable it. See #BB_RTC_CENTURY_TIMESTAMP_MAX
+ **/
+#define BB_RTC_TIMESTAMP_MAX 4102444799
+/**
+ * @brief BBPlayer RTC maximum timestamp (2199-12-31 23:59:59)
+ *
+ * If the century bit is enabled, the maximum timestamp would be
+ * extended by 100 years. LibDragon does no bookkeeping for the
+ * century bit, so switching the century bit has the practical
+ * effect of jumping back or forward 100 years.
+ **/
+#define BB_RTC_CENTURY_TIMESTAMP_MAX 7258118399
+
+/**
+ * @brief Read the time from the BBPlayer RTC as a UNIX timestamp
+ *
+ * @param[out] out pointer to the output time_t
+ *
+ * @retval RTC_ESUCCESS if the operation was successful
+ * @retval RTC_ENOCLOCK if the RTC is not available
+ * @retval RTC_EBADCLOCK if the RTC is not operational
+ * @retval RTC_EBADTIME if the RTC time is not representable
+**/
+int bb_rtc_get_time( time_t *out );
+
+/**
+ * @brief Read the time from the BBPlayer RTC as a UNIX timestamp
+ *
+ * @param new_time the new time to set the BBPlayer RTC to as a UNIX timestamp
+ *
+ * @retval RTC_ESUCCESS if the operation was successful
+ * @retval RTC_ENOCLOCK if the RTC is not available
+ * @retval RTC_EBADCLOCK if the RTC is not operational
+ * @retval RTC_EBADTIME if the RTC cannot represent the new time
+**/
+int bb_rtc_set_time( time_t new_time );
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // LIBDRAGON_BB_RTC_H
diff --git a/include/bbfs.h b/include/bbfs.h
new file mode 100644
index 000000000..699745d30
--- /dev/null
+++ b/include/bbfs.h
@@ -0,0 +1,274 @@
+/**
+ * @file bbfs.h
+ * @author Giovanni Bajo
+ * @brief iQue BBFS flash filesytem
+ * @ingroup ique
+ *
+ * This module contains higher-level functions to interact with BBFS, the flash
+ * filesystem used by the iQue Player.
+ *
+ * Access to the filesystem is provided through the standard C/POSIX file I/O
+ * functions, after calling #bbfs_init to mount the filesystem. To name files
+ * on the filesystem, use the "bbfs:/" prefix.
+ *
+ * Filesystem operations
+ * ---------------------
+ *
+ * Most standard operations are supported, including reading, writing, seeking.
+ * Notice that directories are not supported by the filesystem, so all files
+ * are treated as part of a "root" directory ("/").
+ *
+ * All write operations update the ECC code stored by the NAND on each page,
+ * and all read operations verify the ECC code and use it to correct single-bit
+ * errors. If the ECC code cannot correct the error, the read operation will fail
+ * and errno will be set to EIO. In this case, the data in the filesystem
+ * is likely corrupted.
+ *
+ * In general, writing on a flash filesystem always requires erasing each
+ * block being written (with a block being 16 KiB). This is performed
+ * lazily so that it is possible to write multiple times to the same block
+ * without performance penalty (the block will be written down once). The
+ * library also performs a basic wear leveling to avoid wearing out the
+ * same blocks over and over.
+ *
+ * Truncating files via #truncate is also possible, and can be used to either
+ * reduce or increase the file size. When increasing the file size, the new
+ * data is zeroed out, unless otherwise written. Truncation happens lazily,
+ * so that it is possible to truncate a file to a larger size and then write
+ * data to it without causing a performance penalty.
+ *
+ * ROMs and memory mapping
+ * -----------------------
+ *
+ * One common content of the filesystem is ROMs, which can be memory-mapped
+ * into the emulated PI address space.
+ *
+ * To be able to boot a ROM, the ROM must be written in the filesystem, and
+ * then memory mapped via #nand_mmap. This requires providing the list of the
+ * blocks that contain the ROM data. To facilitate this, #bbfs_get_file_blocks
+ * is provided to retrieve the list of blocks, that can then be passed to
+ * #nand_mmap.
+ *
+ * Since #nand_mmap only supports discontiguous blocks up to a certain limit,
+ * the filesystem tries to keep ROM data in contiguous blocks as much as possible.
+ * To achieve this, the filesystem is virtually split in two areas: a
+ * "large files" area which covers most of the flash and where blocks are
+ * allocated as contiguous as possible, and a "small files" area of the flash
+ * where blocks are allocated randomically (to minimize wear leveling).
+ * The small area is normally just 1 MiB, but will grow automatically when
+ * almost full.
+ *
+ * A file is stored in the large area as soon as its size is larger than 512 KiB.
+ * This means that if you write a ROM file that is larger than 512 KiB, the first
+ * blocks up to 512 KiB will still be stored in the small area and will be fragmented,
+ * but the rest will be stored in the large area and will be contiguous.
+ *
+ * While this is a suboptimal allocation, it will not create any immediate issue.
+ * Anyway, to perform an optimal allocation, there are two possible ways:
+ *
+ * * #ftruncate the file immediately after opening it, to communicate the final
+ * size right away. This will force the filesystem to allocate the file in
+ * the large area and contiguous.
+ * * Use the #IOBBFS_SET_CONTIGUOUS #ioctl to force the filesystem to immediately
+ * use the contiguous block allocation algorithm for the file, irrespective
+ * of its initial (or final) size.
+ *
+ * For instance this is how you can use #ftruncate to obtain the optimal allocation,
+ * for a ROM file:
+ *
+ * \code{.c}
+ * FILE *f = fopen("bbfs:/my_rom.z64", "wb");
+ *
+ * // Truncate the file to the final size right away. Since the final
+ * // size is larger than 512 KiB, the file will immediately switch
+ * // to the large area and contiguous allocation.
+ * ftruncate(fileno(f), rom_size);
+ *
+ * // Write the ROM data
+ * fwrite(rom_data, 1, rom_size, f);
+ *
+ * // Close the file
+ * fclose(f);
+ * \endcode
+ *
+ * To see an example of using the #ioctl instead, see #IOBBFS_SET_CONTIGUOUS.
+ *
+ * Filesytem consistency checks
+ * ----------------------------
+ *
+ * The library also offers a function to check the filesystem consistency:
+ * #bbfs_fsck. This function will scan the filesystem and check for logic errors
+ * in the filesystem structure. If the filesystem is corrupted, it can optionally
+ * try to fix the errors.
+ *
+ * For errors that affect specific files, the portions of the files that can
+ * be recovered are saved with the name "FSCK1234.XXX" where 1234 is a random
+ * number.
+ *
+ * Notice that currently this function does not check the integrity of the
+ * data stored in the filesystem (via ECC), only the filesystem structure.
+ *
+ * ## Error codes
+ *
+ * The filesystem tries to be POSIX compliant, so make sure to check error
+ * codes via errno if an operation fails. In particular, the following error
+ * codes can be returned:
+ *
+ * ### BBFS specific errors
+ *
+ * * EIO: Input/output error during NAND flash operations. This can indicate
+ * hardware failure, corrupted ECC data, or other flash-related issues.
+ * * ENOSPC: No space left on the filesystem. All available blocks have been
+ * allocated and no free space remains for new files or file extensions.
+ * * EEXIST: File exists. Attempting to create a file with O_CREAT|O_EXCL
+ * when a file with the same name already exists.
+ * * ENOENT: No such file. The requested file does not exist, or no more
+ * directory entries are available during directory iteration.
+ * * EINVAL: Invalid argument. The filename format is invalid (longer than
+ * 8.3 format), or invalid path for directory operations (must be "/").
+ * * ENOTTY: Inappropriate ioctl for device. The ioctl request is not
+ * supported by the BBFS filesystem.
+ *
+ * ### Generic filesystem errors
+ *
+ * * EBADF: Bad file descriptor. The file was not opened for the attempted
+ * operation (e.g., trying to read from a file opened only for writing,
+ * or trying to write to a file opened only for reading).
+ *
+ * Additional error codes may be returned by the underlying filesystem layer
+ * in system.c, such as ENOMEM, EPERM, ENFILE, and ENOSYS.
+ */
+
+#ifndef LIBDRAGON_BBFS_H
+#define LIBDRAGON_BBFS_H
+
+#include
+#include
+#include "preview.h"
+#include "ioctl.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Filesystem errors
+ */
+enum bbfs_error_e {
+ BBFS_ERR_SUPERBLOCK = -1, ///< Cannot mount the filesystem (superblock missing or corrupted)
+};
+
+/**
+ * @brief ioctl to notify the filesystem that a certain file must be stored as contiguous.
+ *
+ * This is normally used for ROM files. Since ROM files must be memory mapped
+ * via #nand_mmap, and #nand_mmap only supports discontiguous blocks up to a certain
+ * limit, this #ioctl will inform the filesystem that the current file must be
+ * stored in contiguous blocks as much as possible.
+ *
+ * By default, the filesystem will use a heuristic: smaller files will be stored
+ * fragmented, will larger files (> 512 KiB) will be stored contiguous. This
+ * means that if you write a ROM file that is larger than 512 KiB, the first
+ * blocks up to 512 KiB will still be fragmented, but the rest will be contiguous.
+ *
+ * If you want to avoid this, you can either call #ftruncate immediately after opening
+ * the file to communicate the final size right away, or use this #ioctl to force
+ * to immediately use the contiguous block allocation algorithm.
+ *
+ * \code{.c}
+ * FILE *f = fopen("bbfs:/my_rom.z64", "wb");
+ *
+ * // Notify the filesystem that this file must be stored as contiguous
+ * bool value = true;
+ * ioctl(fileno(f), IOBBFS_SET_CONTIGUOUS, &value);
+ *
+ * // Write the ROM data
+ * fwrite(rom_data, 1, rom_size, f);
+ *
+ * // Close the file
+ * fclose(f);
+ * \endcode
+ *
+ * Calling this #ioctl only affects blocks allocated after the call; there is
+ * no way to change the allocation of blocks that have already been written
+ * (short of truncating the file to zero, and writing it again).
+ *
+ * Calling this #ioctl with the value set to false will revert the file
+ * to the default allocation algorithm based on the heuristics. There is no way
+ * to force a file to be always stored as fragmented.
+ *
+ * See also the top comment of this file for more information.
+ */
+#define IOBBFS_SET_CONTIGUOUS _IO('B', 0)
+
+/**
+ * @brief Get the current block number of an open file
+ *
+ * This function is used to retrieve the current block number of an open file.
+ * The block number is a 16-bit value that represents the current flash block
+ * where the file is being read or written.
+ *
+ * This can be used for debugging purposes, or to implement something similar
+ * to #bbfs_get_file_blocks for an open file, by seeking into the file at each
+ * block boundary.
+ *
+ * \code{.c}
+ * FILE *f = fopen("bbfs:/my_rom.z64", "rb");
+ *
+ * // Get the first block in which the file is stored
+ * // WARNING: make sure to use int16_t for blocks.
+ * int16_t block = 0;
+ * ioctl(fileno(f), IOBBFS_GET_BLOCK, &block);
+ * \endcode
+ */
+#define IOBBFS_GET_BLOCK _IO('B', 1)
+
+/**
+ * @brief Initialize the iQue NAND filesystem library
+ * @preview
+ *
+ * This function mounts the filesystem and initializes the library.
+ * After calling this function, you can access the BBFS files using
+ * the standard C file I/O functions with the "bbfs:/" prefix.
+ *
+ * @return 0 on success, negative value on failure (see #bbfs_error_e)
+ */
+LIBDRAGON_PREVIEW_API
+int bbfs_init(void);
+
+/**
+ * @brief Return the indices of NAND blocks that contain the file data
+ * @preview
+ *
+ * The function lookups the specified file on the filesystem. If found, it
+ * returns a list of block indices that contain the file data. The list is
+ * dynamically allocated and must be freed by the caller.
+ *
+ * The list of block indices is terminated by a -1 value.
+ *
+ * @param filename Filename (*without* filesystem prefix)
+ * @return Allocated array of block indices, or NULL
+ * if file doesn't exist or the filesystem is corrupted
+ */
+LIBDRAGON_PREVIEW_API
+int16_t* bbfs_get_file_blocks(const char *filename);
+
+/**
+ * @brief Verify the integrity of the filesystem, and optionally try to fix it
+ * @preview
+ *
+ * This function checks the integrity of the filesystem. If the filesystem is
+ * corrupted, it can optionally try to fix it.
+ *
+ * @param fix_errors If true, try to fix the errors, otherwise just check
+ * @return number of errors found in the filesystem
+ */
+LIBDRAGON_PREVIEW_API
+int bbfs_fsck(bool fix_errors);
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/bio_sensor.h b/include/bio_sensor.h
new file mode 100644
index 000000000..87797fef1
--- /dev/null
+++ b/include/bio_sensor.h
@@ -0,0 +1,135 @@
+/**
+ * @file bio_sensor.h
+ * @author Christopher Bonhage
+ * @brief Bio Sensor subsystem
+ * @ingroup bio_sensor
+ */
+
+#ifndef LIBDRAGON_BIO_SENSOR_H
+#define LIBDRAGON_BIO_SENSOR_H
+
+#include
+
+#include "preview.h"
+#include "joypad.h"
+
+/**
+ * @defgroup bio_sensor Bio Sensor Subsystem
+ * @ingroup peripherals
+ * @brief Bio Sensor accessory interface.
+ * @author Christopher Bonhage
+ *
+ * Bio Sensor (NUS-A-BIO-JPN) is an N64 controller accessory to read a player's
+ * heartbeat using an infrared sensor. It was only released in Japan, and was
+ * only supported by a single retail game, "Tetris 64". The Bio Sensor is a
+ * video game accessory. It is NOT a medical device and is for entertainment
+ * purposes only.
+ *
+ * The Bio Sensor subsystem provides an interface for reading heartbeat data
+ * from multiple Bio Sensor accessories connected to different joypad ports.
+ * Each port can be independently started and stopped for reading heartbeat data.
+ *
+ * Bio Sensor reads are performed at 60Hz (NTSC) or 50Hz (PAL) using the
+ * vertical interrupt (VI) handler. The subsystem maintains a rolling window of
+ * heartbeat counts to calculate the current heart rate in beats per minute (BPM).
+ *
+ * @{
+ */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Initialize the Bio Sensor subsystem
+ * @preview
+ *
+ * Must be called before starting Bio Sensor reads on any port.
+ *
+ * Registers the VI interrupt handler for periodic sensor reads.
+ * Uses reference counting to support multiple init/close pairs.
+ */
+LIBDRAGON_PREVIEW_API
+void bio_sensor_init(void);
+
+/**
+ * @brief Close the Bio Sensor subsystem
+ * @preview
+ *
+ * Should be called when Bio Sensor functionality is no longer needed.
+ *
+ * Decrements the reference count and unregisters the VI interrupt handler
+ * when the count reaches zero.
+ */
+LIBDRAGON_PREVIEW_API
+void bio_sensor_close(void);
+
+/**
+ * @brief Start reading heartbeat data from a Bio Sensor
+ * @preview
+ *
+ * Initializes the reader context for the specified port and begins
+ * continuous heartbeat monitoring. The sensor will be read at 60Hz (NTSC)
+ * or 50Hz (PAL) via the VI interrupt handler. After starting, it will
+ * take 4-5 seconds to accumulate enough data for a heart rate reading.
+ *
+ * @param port The joypad port with the Bio Sensor accessory attached
+ */
+LIBDRAGON_PREVIEW_API
+void bio_sensor_read_start(joypad_port_t port);
+
+/**
+ * @brief Stop reading heartbeat data from a Bio Sensor
+ * @preview
+ *
+ * Clears the reader context for the specified port and stops all
+ * heartbeat monitoring. Also called automatically if the Bio Sensor
+ * is disconnected during reading.
+ *
+ * @param port The joypad port to stop reading from
+ */
+LIBDRAGON_PREVIEW_API
+void bio_sensor_read_stop(joypad_port_t port);
+
+/**
+ * @brief Check if Bio Sensor is actively reading heartbeat data
+ * @preview
+ *
+ * @param port The joypad port to check
+ * @return true if heartbeat monitoring is active, false if stopped
+ */
+LIBDRAGON_PREVIEW_API
+bool bio_sensor_get_active(joypad_port_t port);
+
+/**
+ * @brief Check if a heartbeat pulse is currently detected
+ * @preview
+ *
+ * @param port The joypad port to check
+ * @return true if the heart is currently beating (pulsing state), false otherwise
+ */
+LIBDRAGON_PREVIEW_API
+bool bio_sensor_get_pulsing(joypad_port_t port);
+
+/**
+ * @brief Calculate the current heart rate in beats per minute (BPM)
+ * @preview
+ *
+ * Calculates the average BPM based on heartbeat counts from multiple
+ * measurement periods. Requires at least 8 measurement periods (4 seconds)
+ * of data before returning a valid BPM. Uses up to 16 periods (8 seconds)
+ * for the rolling average calculation.
+ *
+ * @param port The joypad port to read BPM from
+ * @return Heart rate in beats per minute, or 0 if insufficient data
+ */
+LIBDRAGON_PREVIEW_API
+int bio_sensor_get_bpm(joypad_port_t port);
+
+#ifdef __cplusplus
+}
+#endif
+
+/** @} */ /* bio_sensor */
+
+#endif /* LIBDRAGON_BIO_SENSOR_H */
diff --git a/include/cop0.h b/include/cop0.h
index eb67abbb5..077e77344 100644
--- a/include/cop0.h
+++ b/include/cop0.h
@@ -256,6 +256,50 @@
*/
#define C0_WRITE_WIRED(x) asm volatile("mtc0 %0,$6; nop; nop"::"r"(x))
+/**
+ * @brief Read the COP0 PARITYERR register
+ *
+ * This register is documented as unused on VR4300.
+ */
+ #define C0_PARITYERR() ({ \
+ uint32_t x; \
+ asm volatile("mfc0 %0,$26":"=r"(x)); \
+ x; \
+})
+
+/**
+ * @brief Read the COP0 CACHEERR register
+ *
+ * This register is unused on the hardware and always returns 0.
+ *
+ * @note The emux homebrew spec (ab)uses this register to specify the cause of
+ * an emux exception.
+ */
+#define C0_CACHEERR() ({ \
+ uint32_t x; \
+ asm volatile("mfc0 %0,$27":"=r"(x)); \
+ x; \
+})
+
+/**
+ * @brief Write the COP0 PARITYERR register
+ *
+ * This register is documented as unused on VR4300. The low 8 bits
+ * are writable and are returned on next reads, but they are not used
+ * by the hardware.
+ */
+#define C0_WRITE_PARITYERR(x) asm volatile("mtc0 %0,$26"::"r"(x))
+
+/**
+ * @brief Write the COP0 CACHEERR register
+ *
+ * This register is unused on the hardware. Writes appear to be ignored.
+ *
+ * @note The emux homebrew spec (ab)uses this register to specify the cause of
+ * an emux exception. Writing to it resets the value to 0.
+ */
+#define C0_WRITE_CACHEERR(x) asm volatile("mtc0 %0,$27"::"r"(x))
+
/** @cond */
/* Deprecated version of macros with wrong naming that include "READ" */
#define C0_READ_CR() C0_CAUSE()
@@ -267,6 +311,22 @@
#define C0_STATUS_IE 0x00000001 ///< Status: interrupt enable
#define C0_STATUS_EXL 0x00000002 ///< Status: within exception
#define C0_STATUS_ERL 0x00000004 ///< Status: within error
+#define C0_STATUS_KSU 0x00000018 ///< Status: kernel/user mode
+#define C0_STATUS_UX 0x00000020 ///< Status: 64-bit addressing in user mode
+#define C0_STATUS_SX 0x00000040 ///< Status: 64-bit addressing in supervisor mode
+#define C0_STATUS_KX 0x00000080 ///< Status: 64-bit addressing in kernel mode
+#define C0_STATUS_IM 0x0000FF00 ///< Status: interrupt mask bits
+#define C0_STATUS_IM_SHIFT 8 ///< Status: interrupt mask bits shift
+#define C0_STATUS_SR 0x00010000 ///< Status: soft reset
+#define C0_STATUS_TS 0x00020000 ///< Status: TLB shutdown
+#define C0_STATUS_BEV 0x00400000 ///< Status: use boot exception vectors
+#define C0_STATUS_RE 0x02000000 ///< Status: reverse endian
+#define C0_STATUS_FR 0x04000000 ///< Status: FPU register mode
+#define C0_STATUS_RP 0x08000000 ///< Status: reduced power
+#define C0_STATUS_CU0 0x10000000 ///< Status: coprocessor 0 usable
+#define C0_STATUS_CU1 0x20000000 ///< Status: coprocessor 1 usable
+#define C0_STATUS_CU2 0x40000000 ///< Status: coprocessor 2 usable
+#define C0_STATUS_CU3 0x80000000 ///< Status: coprocessor 3 usable
/* COP0 Cause bits definition. Please refer to MIPS R4300 manual. */
#define C0_CAUSE_BD 0x80000000 ///< Cause: exception triggered in delay slot
@@ -289,17 +349,17 @@
#define C0_INTERRUPT_TIMER C0_INTERRUPT_7 ///< Status/Cause: HW interrupt 7 (Timer)
/**
- * @brief Get the CE value from the COP0 status register
+ * @brief Get the CE value from the COP0 cause register
*
* Gets the Coprocessor unit number referenced by a coprocessor unusable
- * exception from the given COP0 Status register value.
+ * exception from the given COP0 Cause register value.
*/
#define C0_GET_CAUSE_CE(cr) (((cr) & C0_CAUSE_CE) >> 28)
/**
- * @brief Get the exception code value from the COP0 status register value
+ * @brief Get the exception code value from the COP0 cause register value
*/
-#define C0_GET_CAUSE_EXC_CODE(sr) (((sr) & C0_CAUSE_EXC_CODE) >> 2)
+#define C0_GET_CAUSE_EXC_CODE(cr) (((cr) & C0_CAUSE_EXC_CODE) >> 2)
/* Flag bits valid for COP0 ENTRYLO0/ENTRYLO1 registers */
#define C0_ENTRYLO_GLOBAL (1<<0) ///< ENTRYLO: mapping is global (all ASIDs)
@@ -348,4 +408,4 @@
/** @} */
-#endif
\ No newline at end of file
+#endif
diff --git a/include/coroutine.h b/include/coroutine.h
new file mode 100644
index 000000000..1df70f5df
--- /dev/null
+++ b/include/coroutine.h
@@ -0,0 +1,121 @@
+/**
+ * @file coroutine.h
+ * @author Max Bebök
+ * @brief Cooperative coroutines API
+ * @ingroup lowlevel
+ */
+
+#ifndef LIBDRAGON_COROUTINE_H
+#define LIBDRAGON_COROUTINE_H
+
+#include "preview.h"
+#include "ucontext.h"
+#include
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+///@cond
+typedef struct coroutine_s coroutine_t;
+///@endcond
+
+/**
+ * @brief Creates a new coroutine
+ * @preview
+ * This creates a new execution context for the specified function.
+ * After creation, the coroutine can be executed with coro_resume.
+ *
+ * @param fn function to use for the coroutine
+ * @param arg argument to pass into the function
+ * @param stack_size stack size to be allocated
+ * @return created coroutine
+ */
+LIBDRAGON_PREVIEW_API
+coroutine_t *coro_create(void (*fn)(void *), void *arg, size_t stack_size);
+
+/**
+ * @brief Resumes execution
+ * @preview
+ *
+ * Continues to execute the coroutine until the next yield.
+ * If the function reached the end, it will no longer be called.
+ * To check if a coroutine is done, use coro_finished.
+ *
+ * @param co coroutine to resume
+ */
+LIBDRAGON_PREVIEW_API
+void coro_resume(coroutine_t *co);
+
+/**
+ * @brief Yields and gives back control to the caller context.
+ * @preview
+ *
+ * This can be used within the context of a coroutine to pause execution.
+ * When doing so, it gives back control to the code that called coro_resume.
+ */
+LIBDRAGON_PREVIEW_API
+void coro_yield(void);
+
+/**
+ * @brief Puts the current coroutine to sleep.
+ * @preview
+ *
+ * This function will yield, and prevent execution of the current coroutine until
+ * the given time as passed.
+ * Note that the time is a minimum, and the actual time depends on how often you
+ * try to execute the function via coro_resume.
+ *
+ * This function should be preferred over spin-waiting inside the coroutine,
+ * since it avoids a context switch alltogether if the time has not passed yet.
+ *
+ * @param ticks minimum time to wait in ticks
+ */
+LIBDRAGON_PREVIEW_API
+void coro_sleep(uint64_t ticks);
+
+/**
+ * @brief Frees all resources associated with a coroutine.
+ * @preview
+ *
+ * After this function is called it is no longer safe to resume execution,
+ * or interact with the coroutine in any way.
+ *
+ * @param co coroutine to free
+ */
+LIBDRAGON_PREVIEW_API
+void coro_destroy(coroutine_t *co);
+
+/**
+ * @brief Checks if a coroutine has finished execution.
+ * @preview
+ *
+ * @param co coroutine to check
+ * @return true if finished
+ */
+LIBDRAGON_PREVIEW_API
+bool coro_finished(coroutine_t *co);
+
+/**
+ * @brief Returns the currently active coroutine
+ * @preview
+ *
+ * If currently inside a coroutine, it will return the handle.
+ * When outside, it returns NULL.
+ *
+ * This can also be used to detect and change behaviour of a function
+ * depending on if it's running in a coroutine or not.
+ *
+ * @return active coroutine, NULL if in main context
+ */
+LIBDRAGON_PREVIEW_API
+coroutine_t* coro_get_current(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
+
diff --git a/include/cpak.h b/include/cpak.h
new file mode 100644
index 000000000..f90130cc8
--- /dev/null
+++ b/include/cpak.h
@@ -0,0 +1,145 @@
+/**
+ * @file cpak.h
+ * @author Giovanni Bajo
+ * @brief Controller Pak raw access
+ * @ingroup controllerpak
+ *
+ * This module contains lower-level access to the Controller Pak, allowing
+ * direct read and write operations to the pak's memory, and similar
+ * operations that do not involve the filesystem.
+ *
+ * Most applications should use the higher-level filesystem interface
+ * provided by cpakfs.h, which allows to access the notes in the controller
+ * pak as files in a filesystem.
+ */
+
+#ifndef LIBDRAGON_CPAK_H
+#define LIBDRAGON_CPAK_H
+
+#include "preview.h"
+#include "joypad.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Read data from a controller pak
+ * @preview
+ *
+ * This is a low-level operation that reads a sequence of raw bytes from the
+ * controller pak, with no filesystem semantics. A typical use case for this
+ * function is dumping the contents of a controller pak.
+ *
+ * The function will switch to the specified bank (unless it is already in that bank),
+ * and then read the specified number of bytes from the specified address.
+ * The read can be of any length, and the address does not need to be aligned,
+ * but the read cannot cross banks.
+ *
+ * @note There is no way for this function to know how many banks the controller pak has.
+ * The caller must ensure that the bank exists, otherwise the behavior is undefined.
+ * You can use the #cpak_probe_banks function to determine the number of banks,
+ * via hardware probing, or #cpakfs_get_stats to get the number of banks
+ * as recorded in the filesystem.
+ *
+ * @param port Joypad port number (#joypad_port_t)
+ * @param bank Bank number to read from. The caller must ensure
+ * that the bank exists, otherwise the behavior is undefined.
+ * @param address Starting address in the controller pak to read from. Allowed
+ * range is 0-0x7FFF.
+ * @param buffer Buffer to read the data into
+ * @param len Number of bytes to read. The caller must ensure that the length
+ * does not exceed the bank size (0x8000).
+ * @return Number of bytes read, or negative value in case of error
+ * (errno will be set).
+ */
+LIBDRAGON_PREVIEW_API
+int cpak_read(joypad_port_t port, uint8_t bank, uint16_t address, void *buffer, size_t len);
+
+/**
+ * @brief Write data to a controller pak
+ * @preview
+ *
+ * This is a low-level operation that writes a sequence of raw bytes to the
+ * controller pak, with no filesystem semantics. A typical use case for this
+ * function is restoring a dump to a controller pak.
+ *
+ * The function will switch to the specified bank (unless it is already in that bank),
+ * and then write the specified number of bytes to the specified address.
+ * The write can be of any length, and the address does not need to be aligned,
+ * but the write cannot cross banks.
+ *
+ * @note There is no way for this function to know how many banks the controller pak has.
+ * The caller must ensure that the bank exists, otherwise the behavior is undefined.
+ * You can use the #cpak_probe_banks function to determine the number of banks,
+ * via hardware probing, or #cpakfs_get_stats to get the number of banks
+ * as recorded in the filesystem.
+ *
+ * @param port Joypad port number (#joypad_port_t)
+ * @param bank Bank number to write to. The caller must ensure
+ * that the bank exists, otherwise the behavior is undefined.
+ * @param address Starting address in the controller pak to write to. Allowed
+ * range is 0-0x7FFF.
+ * @param buffer Buffer to write the data from
+ * @param len Number of bytes to write. The caller must ensure that the length
+ * does not exceed the bank size (0x8000).
+ * @return Number of bytes written, or negative value in case of error
+ * (errno will be set).
+ */
+LIBDRAGON_PREVIEW_API
+int cpak_write(joypad_port_t port, uint8_t bank, uint16_t address, const void *buffer, size_t len);
+
+/**
+ * @brief Check if a controller pak supports bankswitching or not.
+ * @preview
+ *
+ * This function does not perform I/O, as the accessory detection code already
+ * checked if the controller pak allows for bankswitching or not.
+ *
+ * Notice that this function is meant purely for debugging purposes; the whole
+ * cpak module was designed so that there is no need to change behavior based on
+ * whether the controller pak supports bankswitching or not.
+ *
+ * Notice also that supporting bankswitching does not mean that the controller pak
+ * does actually have multiple banks. Some controller paks (including official
+ * first-party ones) may support bankswitching but only have a single bank available.
+ *
+ * @param port Joypad port to check
+ * @return true if the controller pak supports bankswitching, false otherwise
+ */
+LIBDRAGON_PREVIEW_API
+bool cpak_supports_bankswitching(joypad_port_t port);
+
+/**
+ * @brief Probe the number of banks in a controller pak
+ * @preview
+ *
+ * This function probes the number of banks in a controller pak, by attempting
+ * to switch to each bank and performing a write test to check if the bank
+ * actually exists. It then restores the existing contents in all banks, so
+ * that the operation is non-destructive.
+ *
+ * The standard controller paks have a single bank of 32 KiB, so this function
+ * will always return 1 for them.
+ *
+ * @note This is a low-level operation that is useful during formatting or
+ * similar operations. For most applications, just mount the filesystem and
+ * use the #cpakfs_get_stats function to get the number of banks as recorded
+ * in the filesystem.
+ *
+ * @note The function uses the first block in each bank. With the standard filesystem,
+ * this block is called "label area", is unused, and is often used/corrupted
+ * by games, so even if the cpak is removed during the operation, the
+ * filesystem will not be corrupted.
+ *
+ * @param port Joypad port to check
+ * @return Number of banks found, or negative value in case of error
+ */
+LIBDRAGON_PREVIEW_API
+int cpak_probe_banks(joypad_port_t port);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/cpakfs.h b/include/cpakfs.h
new file mode 100644
index 000000000..4c526cb22
--- /dev/null
+++ b/include/cpakfs.h
@@ -0,0 +1,472 @@
+/**
+ * @file cpakfs.h
+ * @author Giovanni Bajo
+ * @brief Controller Pak filesystem library
+ * @ingroup controllerpak
+ *
+ * This module implements filesystem access to the information stored in the
+ * controller pak, called "notes".
+ *
+ * To check whether a controller pak is present, you can use #joypad_get_accessory_type,
+ * from the joypad module.
+ *
+ * ## Filesystem usage
+ *
+ * Call #cpakfs_mount to mount the controller pak associated to a certain
+ * joypad port, and then use standard C functions to access the notes as
+ * files in the mounted filesystem. The filesystem exposes a flat structure
+ * where all files are located at the root level using the format:
+ * "GAME.PU-filename.ext", where:
+ * - GAME is a 4-character game code (or 8-character hex for non-alphanumeric)
+ * - PU is a 2-character publisher code (or 4-character hex for non-alphanumeric)
+ * - filename is the file name
+ * - ext is the optional file extension
+ *
+ * For instance, to read the note for the game "Doubutsu no Mori" (Animal Crossing),
+ * you can do the following:
+ *
+ * \code{.c}
+ * if (cpakfs_mount(JOYPAD_PORT_1, "cpak1:/") < 0) {
+ * // handle errors, by inspecting errno [...]
+ * return;
+ * }
+ *
+ * // Read the following note:
+ * // Game code: NAFJ
+ * // Publisher code: 01
+ * // Filename: DOUBUTSUNOMORI
+ * // File extension: A
+ * FILE *f = fopen("cpak1:/NAFJ.01-DOUBUTSUNOMORI.A", "rb");
+ * \endcode
+ *
+ * To iterate over the notes in a controller pak, you can use the directory
+ * functions defined in dir.h (eg: #dir_findfirst, #dir_walk, #dir_glob).
+ *
+ * To write a note, create a file in the mounted filesystem using fopen,
+ * and then write the data to it using fwrite.
+ *
+ * To unmount the controller pak filesystem, call #cpakfs_unmount.
+ *
+ * ## Codepage of filenames and extensions
+ *
+ * Filenames and extensions in the controller pak filesystem are encoded
+ * using a special codepage, where only a subset of ASCII characters are
+ * allowed, plus some Kanji characters. Notably, lowercase letters are not
+ * allowed.
+ *
+ * In cpaks, you must specify filenames as UTF-8 strings. If the filename cannot
+ * be represented in the codepage, you will get an error (with errno EINVAL).
+ * The only exception is that lowercase letters are automatically converted
+ * to uppercase, just like a non-case-sensitive filesystem would do.
+ *
+ * The special codepage allows also for embedded NULs in filenames. Since these
+ * cannot be directly represented in C strings, you can use the special character
+ * `\001` as replacement for NUL. For instance, to create a file with an embedded
+ * NUL, you can do the following:
+ *
+ * \code{.c}
+ * // Create a file whose filename is "FOOBAR"
+ * FILE *f = fopen("cpak1:/GAME.01-FOO\001BAR.TXT", "wb");
+ * \endcode
+ *
+ * ## Error codes
+ *
+ * The filesystem tries to be POSIX compliant, so make sure to check error
+ * codes via errno if an operation fails. In particular, the following error
+ * codes can be returned.
+ *
+ * ### Controller Pak specific errors
+ *
+ * * EIO: Input/output error on the wire. The serial connection is faulty,
+ * so either the cable is damaged or the cpak is electrically unstable.
+ * * ENXIO: The controller pak or the whole joypad has been abruptly disconnected
+ * during the operation.
+ * * ENODEV: the controller pak appears not to contain a valid filesystem, or
+ * it was corrupted. Use #cpakfs_fsck to try recovering the contents. Also
+ * returned when no controller pak is present on the specified port.
+ * * ENOSPC: No space left on the controller pak to allocate new pages for
+ * writing to a file.
+ * * EMFILE: Too many files. The note table is full (maximum 16 notes per pak).
+ * * EFTYPE: Inappropriate file type or format. The filesystem structure is
+ * corrupted, with invalid FAT entries or file chains.
+ * * EEXIST: File exists. Attempting to create a file with O_CREAT|O_EXCL
+ * when a file with the same name already exists.
+ * * ENOENT: No such file or directory. The requested file does not exist,
+ * or no more directory entries are available during directory iteration.
+ * * EINVAL: Invalid argument. For directory operations, the path is not "/"
+ * (the root directory). For file operations, the filename format is invalid.
+ * Also returned for invalid file descriptors or null pointers.
+ *
+ * ### Generic filesystem errors
+ *
+ * * EBADF: Bad file descriptor. The file was not opened for the attempted
+ * operation (e.g., trying to read from a file opened only for writing).
+ * * ENOMEM: Not enough memory available for filesystem operations (e.g., when
+ * mounting a filesystem or allocating file handles).
+ * * EPERM: Operation not permitted. Returned when trying to mount a filesystem
+ * with a prefix that's already in use, or when trying to unmount a filesystem
+ * that doesn't exist.
+ * * ENFILE: Too many open files in the system. The file handle table is full.
+ * * ENOSYS: Function not implemented. Returned for unsupported operations
+ * on filesystems that don't implement certain features (e.g., some file
+ * operations if the filesystem doesn't support them).
+ */
+#ifndef LIBDRAGON_CPAKFS_H
+#define LIBDRAGON_CPAKFS_H
+
+#include
+#include
+#include
+#include "preview.h"
+#include "ioctl.h"
+#include "joypad.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+
+/**
+ * @brief Get the current bank/page of an open file in cpakfs
+ *
+ * This ioctl allows you to retrieve the current position of an open file
+ * in terms of bank and page, packed into a single uint16_t value.
+ * The high byte contains the bank number, the low byte contains the page number.
+ *
+ * Example usage:
+ *
+ * uint16_t bankpage;
+ * ioctl(fileno(f), IOCPAKFS_GET_PAGE, &bankpage);
+ * int bank = bankpage >> 8;
+ * int page = bankpage & 0xFF;
+ *
+ * If the file is not valid, the ioctl returns -1 and errno is set.
+ */
+#define IOCPAKFS_GET_PAGE _IO('K', 0)
+
+/**
+ * @brief Usage statistics for a Controller Pak
+ *
+ * This structure is returned by #cpakfs_get_stats and contains the usage
+ * statistics for a controller pak.
+ */
+typedef struct {
+ struct {
+ int total; ///< Total number of pages in the controller pak
+ int used; ///< Number of pages used in the controller pak
+ } pages; ///< Statistics on pages in the controller pak
+ struct {
+ int total; ///< Total number of notes in the controller pak
+ int used; ///< Number of notes used in the controller pak
+ } notes; ///< Statistics on notes in the controller pak
+ int num_banks; ///< Number of banks in the controller pak (as recorded in the filesystem)
+} cpakfs_stats_t;
+
+/** @brief Cpak filesystem issue levels */
+typedef enum {
+ /** The reported issue does not prevent a filesystem from mounting or otherwise working.
+ * It's just advisory.
+ * Example: a backup copy isn't synchronized with the main copy; a reserved sector
+ * isn't correctly marked as such, etc.
+ */
+ CPAKFS_LEVEL_INFO = 0,
+
+ /**
+ * The issue can prevent correct interpretation of the filesystem, but can be
+ * recovered automatically.
+ */
+ CPAKFS_LEVEL_WARNING = 1,
+
+ /**
+ * The issue is critical; integrity of the filesystem is potentially compromised
+ * and data may be lost after recovery.
+ */
+ CPAKFS_LEVEL_ERROR = 2,
+} cpakfs_issue_level_t;
+
+/** @brief Cpak filesystem issues */
+typedef enum {
+ CPAKFS_ISSUE_FSID_CHECKSUM_FAILURE = 1, ///< Cannot find an ID sector with correct checksum
+ CPAKFS_ISSUE_FSID_DEXDRIVE = 2, ///< ID sector has a known-corrupted checksum generated by DexDrive
+ CPAKFS_ISSUE_FSID_CORRUPTED_BACKUP = 3, ///< ID sector backup is corrupted (can be recovered from main)
+ CPAKFS_ISSUE_FSID_WRONG_DEVICE_ID = 4, ///< Device ID is not valid; this is just advisory so it can be ignored
+
+ CPAKFS_ISSUE_FAT_CHECKSUM_FAILURE = 5, ///< FAT page has invalid checksum
+ CPAKFS_ISSUE_FAT_UNSYNCHRONIZED = 6, ///< FAT page has unsynchronized copies
+ CPAKFS_ISSUE_FAT_INVALID_RESERVED = 7, ///< Reserved page has invalid FAT entry
+ CPAKFS_ISSUE_FAT_INVALID_ENTRY = 8, ///< Invalid FAT entry
+ CPAKFS_ISSUE_FAT_INVALID_BANK = 9, ///< FAT entry has invalid bank number
+
+ CPAKFS_ISSUE_NOTE_INVALID_GAMECODE = 10, ///< Note has invalid gamecode
+ CPAKFS_ISSUE_NOTE_INVALID_PUBCODE = 11, ///< Note has invalid publisher code
+ CPAKFS_ISSUE_NOTE_INVALID_FIRST_PAGE = 12, ///< Note has invalid first page
+ CPAKFS_ISSUE_NOTE_INVALID_CHARSET = 13, ///< Note has invalid characters in filename or extension
+ CPAKFS_ISSUE_NOTE_NOT_OCCUPTED = 14, ///< Note is not correctly marked as occupied
+
+ CPAKFS_ISSUE_CHAIN_INFINITE_LOOP = 15, ///< Chain has infinite loop
+ CPAKFS_ISSUE_CHAIN_NO_TERMINATOR = 16, ///< Chain does not end with terminator
+ CPAKFS_ISSUE_CHAIN_COLLISION = 17, ///< Chain collides with another chain
+ CPAKFS_ISSUE_CHAIN_ORPHANED = 18, ///< Orphaned chain found
+} cpakfs_issue_t;
+
+/** @brief Cpak filesystem report callback */
+typedef void (*cpakfs_report_fn)(void *ctx, cpakfs_issue_t issue, cpakfs_issue_level_t level, const char *fmt, ...);
+
+/**
+ * @brief Mount the controller pak as filesystem
+ * @preview
+ *
+ * This function mounts the contents of a controller pak as a virtual
+ * filesystem, with the specified prefix. After this function successfully
+ * return, it is possible to access the notes in the cpak using standard
+ * C functions like fopen.
+ *
+ * \code{.c}
+ * if (cpak_mount(JOYPAD_PORT_1, "cpak1:/") < 0) {
+ * // handle errors, by inspecting errno [...]
+ * return;
+ * }
+ *
+ * // Read the following note:
+ * // Game code: NAFJ
+ * // Publisher code: 01
+ * // Filename: DOUBUTSUNOMORI
+ * // File extension: A
+ * FILE *f = fopen("cpak1:/NAFJ.01/DOUBUTSUNOMORI.A");
+ * \endcode
+ *
+ * The virtual filesystem structure is as follows:
+ * * Root directory contains no files, only subdirectories. The name of the
+ * subdirectories is a 4.2 ASCII string that encode the game code and
+ * publisher code (eg: "NSME.01")
+ * * Within each subdirectory, you can find one or multiple files that are
+ * the notes found in the cpak. The filenames are UTF-8 strings that must
+ * adhere to the special cpak charset.
+ * * Empty directories do not exist.
+ *
+ * In case of error while mounting the filesystem, errno is set as follows:
+ *
+ * * EIO: Input/output error on the wire. The serial connection is faulty,
+ * so either the cable is damaged or the cpak is electrically unstable.
+ * * ENXIO: The controller pak or the whole joypad has been abruptly disconnected
+ * during the operation.
+ * * ENODEV: the controller pak appears not to contain a valid filesystem, or
+ * it was corrupted. Use #cpakfs_fsck to try recovering the contents.
+ *
+ * @param port Cpak to mount, identified by the joypad port
+ * @param prefix Filesystem prefix to use for mounting. Suggested
+ * name is "cpakN:/" where "N" is the controller
+ * port (1..4).
+ * @return 0 if success, negative value in case of error (and errno is set)
+ */
+LIBDRAGON_PREVIEW_API
+int cpakfs_mount(joypad_port_t port, const char *prefix);
+
+/**
+ * @brief Unmount the controller pak filesystem
+ * @preview
+ *
+ * This function unmounts the controller pak filesystem, waiting for all
+ * pending operations to complete.
+ *
+ * @param port The controller pak to unmount
+ * @return 0 if success, negative value in case of error (and errno is set)
+ */
+LIBDRAGON_PREVIEW_API
+int cpakfs_unmount(joypad_port_t port);
+
+/**
+ * @brief Read the serial number of a controller pak
+ * @preview
+ *
+ * This function reads the 24-byte serial number of a controller pak.
+ * This is a unique identifier that can be used to distinguish between
+ * different controller paks. It is normally generated with random data
+ * when the controller pak is formatted, so it does not contain printable
+ * characters.
+ *
+ * @param port The controller pak to read the serial from
+ * @param serial The buffer where to store the serial number (24 bytes)
+ * @return 0 if the serial was successfully read
+ * @return negative if an error occurred (eg: no cpak on the specified port),
+ * and errno is set accordingly.
+ */
+LIBDRAGON_PREVIEW_API
+int cpakfs_get_serial(joypad_port_t port, uint8_t serial[24]);
+
+/**
+ * @brief Read the usage state of a controller pak
+ * @preview
+ *
+ * @note The filesystem must be mounted before calling this function.
+ *
+ * @param port The controller pak to read the usage state from
+ * @param stats The structure where to store the usage statistics
+ * @return 0 if the serial was successfully read
+ * @return negative if an error occurred (eg: no cpak on the specified port),
+ * and errno is set accordingly.
+ */
+LIBDRAGON_PREVIEW_API
+int cpakfs_get_stats(joypad_port_t port, cpakfs_stats_t *stats);
+
+/**
+ * @brief Check the integrity of a controller pak
+ * @preview
+ *
+ * This function checks the integrity of a controller pak filesystem, and
+ * optionally tries also to repair it if unreadable, and recover as much as
+ * possible.
+ *
+ * Found issues can be reported via the report callback, which is called
+ * for each issue found.
+ *
+ * The return value will report the number of issues found whose level is at least
+ * CPAKFS_LEVEL_WARNING. In fact, issues of kind CPAKFS_LEVEL_INFO, while fixable,
+ * do not impact filesystem integrity in any way (for instance, a backup copy
+ * of some metadata might be corrupted, but the main copy is still valid).
+ *
+ * @param port The controller pak to check the integrity of
+ * @param fix_errors Whether to fix the errors found
+ * @param report Optional callback to report issues found during the check
+ * @param report_ctx Optional context to pass to the report callback
+ * @return positive Number of issues found (and possibly fixed if @p fix_errors is true).
+ * @return 0 No issues found.
+ * @return negative If a physical error occurred (eg: no cpak on the specified port),
+ * and errno will be set accordingly.
+ */
+LIBDRAGON_PREVIEW_API
+int cpakfs_fsck(joypad_port_t port, bool fix_errors,
+ cpakfs_report_fn report, void *report_ctx);
+
+/**
+ * @brief Format a controller pak
+ * @preview
+ *
+ * This function formats a controller pak, resetting to a pristine, valid
+ * empty state. By default, the function only erases the filesystem
+ * metadata but does not purge content from all the pages. This is OK for
+ * most use cases.
+ *
+ * If @p erase is true, all pages are written with zeros, effectively erasing
+ * all the content from the controller pak.
+ *
+ * @param port The controller pak to format
+ * @param erase Whether to erase all content or just the metadata
+ * @return 0 on success, negative on error
+ */
+LIBDRAGON_PREVIEW_API
+int cpakfs_format(joypad_port_t port, bool erase);
+
+
+/**
+ * @brief Cpakfs path components
+ *
+ * A file path on the cpak filesystem is composed of four components:
+ *
+ * - Game code: this would normally match the game ID in the ROM header, so
+ * it is a 4-character string made of uppercase letters and digits. In some
+ * cases (eg: Datel Gameshark), it is made of non-printable characters.
+ * - Publisher code: this is a 2-character string that identifies the publisher
+ * of the game. It was a global registry assigned by Nintendo. Like the game
+ * code, this is normally just uppercase letters and digits.
+ * - Filename: this is a character string in a custom codepage. The codepage
+ * is a subset of ASCII, notably excluding lowercase letters and common
+ * characters like the underscore, but including some Katana characters.
+ * Notice also that the string might contain embedded NULs. The actual
+ * filename length is determined by the position of the last non-zero byte.
+ * - Extension: an optional 4-character string to further identify the file.
+ * Sometimes games use the same filename for all saves and just put an
+ * index in the extension, like "SAVE.0", "SAVE.1", etc. The actual
+ * extension length is determined by the position of the last non-zero byte.
+ *
+ * Cpakfs in libdragon offers a simplified interface to manipulate paths
+ * using what it called a "full name". A full name is a UTF-8 string that
+ * contains all the components, composed as follows:
+ *
+ * GAME.PU-filename.ext
+ *
+ * The full name is also the string that you can use when manipulating notes
+ * via the filesystem interface (eg: fopen, #dir_walk, etc).
+ *
+ * You can use #cpakfs_path_parse and #cpakfs_path_format to convert from
+ * and to the UTF-8 full name and the #cpakfs_path_t structure. Normally,
+ * users of cpakfs will only need to use the full name, but some advanced
+ * uses might require accessing the individual components.
+ */
+typedef struct {
+ uint8_t gamecode[4]; ///< Game code (4 chars, normally ASCII)
+ uint8_t pubcode[2]; ///< Publisher code (2 chars, normally ASCII)
+ uint8_t filename[16]; ///< Filename in N64 codepage (zero-padded)
+ uint8_t ext[4]; ///< Extension in N64 codepage (zero-padded)
+} cpakfs_path_t;
+
+/** @brief #cpakfs_path_parse result codes */
+typedef enum {
+ CPAKFS_PARSE_OK = 0, ///< Path parsed successfully
+ CPAKFS_PARSE_ERR_GAMECODE_TOO_SHORT = -1, ///< Game code too short in the path
+ CPAKFS_PARSE_ERR_GAMECODE_TOO_LONG = -2, ///< Game code too long in the path
+ CPAKFS_PARSE_ERR_GAMECODE_CHAR = -3, ///< Invalid game code character
+ CPAKFS_PARSE_ERR_PUBCODE_TOO_SHORT = -4, ///< Publisher code too short in the path
+ CPAKFS_PARSE_ERR_PUBCODE_TOO_LONG = -5, ///< Publisher code too long in the path
+ CPAKFS_PARSE_ERR_PUBCODE_CHAR = -6, ///< Invalid publisher code character in the path
+ CPAKFS_PARSE_ERR_FILENAME_TOO_SHORT = -7, ///< Filename too short (empty) in the path
+ CPAKFS_PARSE_ERR_FILENAME_TOO_LONG = -8, ///< Filename too long in the path
+ CPAKFS_PARSE_ERR_FILENAME_CHAR = -9, ///< Invalid filename character in the path
+ CPAKFS_PARSE_ERR_EXTENSION_TOO_LONG = -10, ///< Extension too long in the path
+ CPAKFS_PARSE_ERR_EXTENSION_CHAR = -11, ///< Invalid extension character in the path
+} cpakfs_parse_err_t;
+
+/**
+ * @brief Parse a cpakfs fullpath into its components
+ * @preview
+ *
+ * This function parses a full path in the format "GAMECODE.PU-filename.ext"
+ * and extracts the game code, publisher code, filename, and extension into
+ * the provided #cpakfs_path_t structure.
+ *
+ * Parsing a path might fail if the input full path is not in the expected
+ * format (eg: too long components), or if the filename or extension contains
+ * unsupported characters. If @p error_pos is provided, it will be set to point
+ * to the first character that caused the error.
+ *
+ * @param utf8_fullname Input full path to parse (UTF-8 string)
+ * @param path Output path structure to populate
+ * @param error_pos Optional output parameter to indicate the position
+ * of the first error in the input string, if parsing fails.
+ * @result 0 on success, or a negative value indicating the type of error. See
+ * #cpakfs_parse_err_t for details.
+ */
+LIBDRAGON_PREVIEW_API
+cpakfs_parse_err_t cpakfs_path_parse(const char *utf8_fullname, cpakfs_path_t *path, const char **error_pos);
+
+/**
+ * @brief Format a cpakfs path structure into a full path string
+ * @preview
+ *
+ * This function will always succeed, as it will find a way to format any
+ * combination of game code, publisher code, filename, and extension, including
+ * invalid byte sequences according to the expected codepages and rules, falling
+ * back to hex encoding if necessary.
+ *
+ * For instance, if the gamecode is not a valid ASCII string but a random sequence
+ * of bytes like eg: `"\x04\x05\x06\x07"`, the function will format it as
+ * `"04050607.PU-filename.ext"`.
+ *
+ * So it is guaranteed that any input path structure can be formatted into a valid
+ * UTF-8 string as full name, and that the resulting string will always be
+ * parsable back to the same path structure using #cpakfs_path_parse.
+ *
+ * @param path Input path structure to format
+ * @param utf8_fullname Output buffer to receive the formatted full path (UTF-8 string)
+ * @param buflen Size of the output buffer (including space for null terminator)
+ * @return 0 on success, -1 if the buffer was too small
+ */
+LIBDRAGON_PREVIEW_API
+int cpakfs_path_format(const cpakfs_path_t *path, char *utf8_fullname, int buflen);
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
\ No newline at end of file
diff --git a/include/dd.h b/include/dd.h
new file mode 100644
index 000000000..e3a73b635
--- /dev/null
+++ b/include/dd.h
@@ -0,0 +1,108 @@
+/**
+ * @file dd.h
+ * @author Giovanni Bajo
+ * @author Christopher Bonhage
+ */
+#ifndef LIBDRAGON_DD_H
+#define LIBDRAGON_DD_H
+
+#include
+#include
+#include
+#include "preview.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+///@cond
+extern bool dd_found;
+///@endcond
+
+/** @brief Base address of the 64DD ASIC */
+#define DD_ASIC_BASE 0x05000500
+/** @brief Data register address for 64DD ASIC */
+#define DD_ASIC_DATA (DD_ASIC_BASE + 0x0)
+/** @brief Status register address for 64DD ASIC */
+#define DD_ASIC_STATUS (DD_ASIC_BASE + 0x8)
+/** @brief Write command register address for 64DD ASIC */
+#define DD_ASIC_WCMD (DD_ASIC_BASE + 0x8)
+/** @brief Write control register address for 64DD ASIC */
+#define DD_ASIC_WCTRL (DD_ASIC_BASE + 0x10)
+
+/** @brief Writes a 16-bit value to the 64DD ASIC
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+void dd_write(uint32_t address, uint16_t value);
+
+/** @brief Reads a 16-bit value from the 64DD ASIC
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+uint16_t dd_read(uint32_t address);
+
+/** @brief 64DD command codes */
+typedef enum {
+ DD_CMD_CLEAR_RESET_FLAG = 0x09, ///< Clear reset flag command
+ DD_CMD_RTC_SET_YEARMONTH = 0x0f, ///< Set RTC year/month command
+ DD_CMD_RTC_SET_DAYHOUR = 0x10, ///< Set RTC day/hour command
+ DD_CMD_RTC_SET_MINSEC = 0x11, ///< Set RTC minute/second command
+ DD_CMD_RTC_GET_YEARMONTH = 0x12, ///< Get RTC year/month command
+ DD_CMD_RTC_GET_DAYHOUR = 0x13, ///< Get RTC day/hour command
+ DD_CMD_RTC_GET_MINSEC = 0x14, ///< Get RTC minute/second command
+} dd_cmd_t;
+
+/** @brief Sends a command to the 64DD ASIC
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+uint16_t dd_command(dd_cmd_t cmd);
+
+/** @brief 64DD RTC minimum timestamp (1996-01-01 00:00:00) */
+#define DD_RTC_TIMESTAMP_MIN 820454400
+/** @brief 64DD RTC maximum timestamp (2095-12-31 23:59:59) */
+#define DD_RTC_TIMESTAMP_MAX 3976214399
+
+/**
+ * @brief Read the time from the 64DD real-time clock as a UNIX timestamp
+ * @preview
+ *
+ * @param[out] out pointer to the output time_t
+ *
+ * @retval RTC_ESUCCESS if the operation was successful
+ * @retval RTC_ENOCLOCK if the RTC is not available
+ * @retval RTC_EBADCLOCK if the RTC is not operational
+ * @retval RTC_EBADTIME if the RTC time is not representable
+ */
+LIBDRAGON_PREVIEW_API
+int dd_rtc_get_time( time_t *out );
+
+/**
+ * @brief Set the date/time on the 64DD real-time clock.
+ * @preview
+ *
+ * @param new_time the new RTC time as a UNIX timestamp
+ *
+ * @retval RTC_ESUCCESS if the operation was successful
+ * @retval RTC_ENOCLOCK if the RTC is not available
+ * @retval RTC_EBADCLOCK if the RTC is not operational
+ * @retval RTC_EBADTIME if the RTC cannot represent the new time
+ */
+LIBDRAGON_PREVIEW_API
+int dd_rtc_set_time( time_t new_time );
+
+/** @brief Checks if 64DD hardware is present
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline bool sys_dd(void) {
+ return dd_found;
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // LIBDRAGON_DD_H
+
diff --git a/include/debug.h b/include/debug.h
index a35b70bc8..a93726f27 100644
--- a/include/debug.h
+++ b/include/debug.h
@@ -14,6 +14,8 @@
extern "C" {
#endif
+#ifdef N64
+
/**
* @brief Flag to activate the USB logging channel.
*
@@ -42,7 +44,9 @@ extern "C" {
/**
* @brief Flag to activate the logging channel in emulators.
*
- * Currently relies on ISViewer support from the emulator.
+ * The logging is done using @ref emux if available,
+ * or through ISViewer otherwise if available.
+ *
* ISViewer was a real development cartridge that was used in the 90s
* to debug N64 development. It is emulated by a few emulators to ease
* the work of homebrew developers.
@@ -53,10 +57,10 @@ extern "C" {
*
* Supported emulators:
*
- * * cen64 (https://github.com/n64dev/cen64) - run with -is-viewer command line flag
- * * Ares (https://ares-emulator.github.io)
- * * simple64 (https://simple64.github.io)
- * * dgb-n64 (https://github.com/Dillonb/n64)
+ * * cen64 (https://github.com/n64dev/cen64) - ISViewer only. Run with -is-viewer command line flag
+ * * Ares (https://ares-emulator.github.io) - emux and ISViewer
+ * * Gopher64 (https://loganmc10.itch.io/gopher64) - ISViewer only
+ * * dgb-n64 (https://github.com/Dillonb/n64) - ISViewer only
*
*/
#define DEBUG_FEATURE_LOG_EMU (1 << 1)
@@ -142,9 +146,24 @@ extern "C" {
#ifndef NDEBUG
- /** @brief Initialize USB logging. */
+ /**
+ * @brief Initialize USB logging.
+ *
+ * This function initializes the USB logging channel. It is used to log messages
+ * to the USB port of the development cartridge.
+ *
+ * @return true if the USB logging channel was initialized successfully, false otherwise.
+ * @note This function is not available on iQue Player
+ */
bool debug_init_usblog(void);
- /** @brief Initialize emulator logging. */
+ /**
+ * @brief Initialize ISViewer logging.
+ *
+ * This function initializes the ISViewer logging channel. It is used to log messages
+ * to emulators that support it.
+ *
+ * @return true if the ISViewer logging channel was initialized successfully, false otherwise
+ */
bool debug_init_emulog(void);
/** @brief Initialize SD logging. */
bool debug_init_sdlog(const char *fn, const char *openfmt);
@@ -182,14 +201,19 @@ extern "C" {
/**
* @brief Write a message to the debugging channel.
*
- * This macro is a simple wrapper over fprintf(stderr), to write
+ * This function is a simple wrapper over fprintf(stderr), to write
* a debugging message through all the activated debugging channels.
*
- * Writing directly to stderr is perfectly supported; this macro
+ * Writing directly to stderr is perfectly supported; this function
* only simplifies disabling all debugging features, because it
* is disabled when compiling with NDEBUG.
+ *
+ * Moreover, this function also has a special codepath to allow
+ * writing debug messages during interrupts or exceptions, when standard
+ * I/O functions cannot be used safely.
*/
- #define debugf(msg, ...) fprintf(stderr, msg, ##__VA_ARGS__)
+ __attribute__((format(printf, 1, 2)))
+ void debugf(const char *msg, ...);
/**
* @brief assertf() is like assert() with an attached printf().
@@ -209,11 +233,11 @@ extern "C" {
})
#else
- #define debug_init(ch) ({ false; })
- #define debug_init_usblog() ({ false; })
- #define debug_init_emulog() ({ false; })
- #define debug_init_sdlog(fn,fmt) ({ false; })
- #define debug_init_sdfs(prefix,np) ({ false; })
+ #define debug_init(ch) ((void)(false), false)
+ #define debug_init_usblog() ((void)(false), false)
+ #define debug_init_emulog() ((void)(false), false)
+ #define debug_init_sdlog(fn,fmt) ((void)(false), false)
+ #define debug_init_sdfs(prefix,np) ((void)(false), false)
#define debugf(msg, ...) ({ })
#define assertf(expr, msg, ...) ({ })
#endif
@@ -259,6 +283,30 @@ void debug_backtrace(void);
void debug_assert_func_f(const char *file, int line, const char *func, const char *failedexpr, const char *msg, ...)
__attribute__((noreturn, format(printf, 5, 6)));
+
+#else /* N64 */
+
+#include
+
+// Lots of tools code include this file transitively. Make sure we expose the
+// bare minimum assertion APIs.
+
+#define assertf(expr, msg, ...) ({ \
+ if (!(expr)) { \
+ fprintf(stderr, "ASSERTION FAILED: "); \
+ fprintf(stderr, msg, ##__VA_ARGS__); \
+ assert(expr); \
+ abort(); \
+ } \
+})
+
+#define debugf(msg, ...) ({ \
+ fprintf(stderr, msg, ##__VA_ARGS__); \
+})
+
+#endif /* N64 */
+
+
#ifdef __cplusplus
} /* extern "C" */
#endif
diff --git a/include/dir.h b/include/dir.h
index b93846657..eabf7ad98 100644
--- a/include/dir.h
+++ b/include/dir.h
@@ -116,7 +116,7 @@ enum {
* @param fn The full absolute filename of the file or directory
* @param dir The directory entry structure with information about the file or directory
* @param data User data passed to #dir_walk
- * @return int The return value determines the behavior of the directory walk (one of DIR_WALK constants)
+ * @return The return value determines the behavior of the directory walk (one of DIR_WALK constants)
*/
typedef int (*dir_walk_callback_t)(const char *fn, dir_t *dir, void *data);
diff --git a/include/display.h b/include/display.h
index 8c349bbb6..db4123db3 100644
--- a/include/display.h
+++ b/include/display.h
@@ -2,15 +2,59 @@
* @file display.h
* @author Jennifer Taylor
* @author Giovanni Bajo
- * @author SpookyIluha
* @brief Display Subsystem
* @ingroup display
+ *
+ * The display subsystem module is responsible for initializing the proper video
+ * mode for displaying 2D, 3D and software graphics. It is the higher level module
+ * that most applications should use to configure the video output.
+ *
+ * To set up video on the N64, code should call #display_init with the appropriate
+ * options. It is a powerful API with many options (see #resolution_t) but
+ * it can still be a simple one-liner for basic cases. It allows to configure
+ * any resolution including custom ones, any aspect ratio, any kind of
+ * letterboxing, overscan, interlacing, and so on. It also allocates a
+ * framebuffer chain.
+ *
+ * Once the display has been set, a surface can be requested from the display
+ * subsystem using #display_get. To draw to the acquired surface, code should
+ * use functions present in the @ref rdpq or @ref graphics modules (the latter
+ * being just a simpler, CPU-only small graphic library).
+ *
+ * Once drawing to a surface is complete, the rendered graphic can be displayed
+ * to the screen using #display_show. Once code has finished rendering all
+ * graphics, #display_close can be used to shut down the display subsystem.
+ *
+ * To obtain a Z-buffer, simply call #display_get_zbuf(). This is preferrable
+ * to manually allocating it, as display.h will manage the memory allocations
+ * spreading them across RDRAM banks to maximize rendering performance.
+ *
+ * ## Frame counter, delta time and frame limiter
+ *
+ * You can acquire at any time a very accurate frame rate estimation using
+ * #display_get_fps. This is measure the actual time that it takes for a
+ * frame to reach the display, rather than the time it takes to be calculated,
+ * so it is more accurate especially when there's a high variance or long
+ * framebuffer chains.
+ *
+ * Use #display_get_delta_time to get an estimation of the best delta time
+ * that can be used to calculate the next frame. This builds up on the
+ * FPS calculator, so again it is more accurate and smoother than just
+ * taking the time yourself in your main loop.acos
+ *
+ * If you want to limit frame rate, call #display_set_fps_limit. This allows
+ * to limit the frame rate to a specific value, and not only submultiples of
+ * the TV refresh rate. For instance, you can call "display_set_fps_limit(45)"
+ * and it will work as expected, as smooth as possible.
*/
#ifndef __LIBDRAGON_DISPLAY_H
#define __LIBDRAGON_DISPLAY_H
+
#include
#include
+#include "preview.h"
+#include "vi.h"
/**
* @defgroup display Display Subsystem
@@ -20,22 +64,25 @@
*
* The display subsystem handles interfacing with the video interface (VI)
* and the hardware rasterizer (RDP) to allow software and hardware graphics
- * operations. It consists of the @ref display, the @ref graphics and the
- * @ref rdp modules. A separate module, the @ref console, provides a rudimentary
- * console for developers. Only the display subsystem or the console can be
- * used at the same time. However, commands to draw console text to the display
- * subsystem are available.
- *
- * The display subsystem module is responsible for initializing the proper video
- * mode for displaying 2D, 3D and software graphics. To set up video on the N64,
- * code should call #display_init with the appropriate options. Once the display
- * has been set, a surface can be requested from the display subsystem using
- * #display_get. To draw to the acquired surface, code should use functions
- * present in the @ref graphics and the @ref rdp modules. Once drawing to a surface
- * is complete, the rendered graphic can be displayed to the screen using
- * #display_show. Once code has finished rendering all graphics, #display_close can
- * be used to shut down the display subsystem.
- *
+ * operations. It consists of:
+ *
+ * * vi.h: the low-level VI programming module. This is useful for people
+ * wanting to tinker with low-level video programming such as custom
+ * timings or weird effects,
+ * * display.h: the higher-level display module. This is the basic module
+ * most libdragon applications will use to setup a screen resolution,
+ * a framebuffer chain, and draw onto the screen. display.h builds upon
+ * vi.h for low-level access.
+ * * graphics.h: this is a simple graphics library that uses CPU to draw on
+ * the screen. It is meant for very simple graphic applications like basic
+ * test ROMs, or situations where you don't want to touch the RDP
+ * (eg: exception handlers). Most applications should instead use
+ * the @ref rdpq library to draw graphics.
+ * * console.h: it provides a rudimentary textual console for developers,
+ * where you can simply write text using printf(). This is useful just for
+ * very basic tests. Notice that this console is not meant to be running
+ * while the display subsystem is active, as it will conflict with it.
+ *
*/
///@cond
@@ -47,36 +94,91 @@ typedef struct surface_s surface_t;
* @{
*/
+/** @brief Valid interlace modes */
+typedef enum {
+ /** @brief Video output is not interlaced */
+ INTERLACE_OFF,
+ /** @brief Video output is interlaced and buffer is swapped on odd and even fields */
+ INTERLACE_HALF,
+ /** @brief Video output is interlaced and buffer is swapped only on even fields */
+ INTERLACE_FULL,
+} interlace_mode_t;
+
/**
* @brief Video resolution structure
*
- * You can either use one of the pre-defined constants
- * (such as #RESOLUTION_320x240) or define a custom resolution.
+ * This structure allows to configure the video resolution, which includes both
+ * the framebuffer size and some parameters of how the framebuffer is displayed
+ * on the screen (aspect ratio, TV overscan margins, etc.).
+ *
+ * Most users should just use one of the pre-defined constants (such as
+ * #RESOLUTION_320x240), but it is possible to configure custom resolutions
+ * by manually filling fields in this structure.
*/
typedef struct {
- /** @brief Screen width (must be between 2 and 800) */
+ /** @brief Framebuffer width (must be between 2 and 800) */
int32_t width;
- /** @brief Screen height (must be between 1 and 720) */
+ /** @brief Framebuffer height (must be between 1 and 720) */
int32_t height;
- /** @brief True if interlaced mode enabled */
- bool interlaced;
+ /** @brief Interlace mode */
+ interlace_mode_t interlaced;
+ /**
+ * @brief Configure the desired aspect ratio of the output display picture.
+ * @preview
+ *
+ * By default (when this value is 0), the framebuffer will be displayed as
+ * a 4:3 picture, irrespective of its width and height. By tweaking this
+ * value, the image will instead be letterboxed (with black bars) to
+ * achieve the requested aspect ratio.
+ *
+ * For instance, to display the framebuffer as letterboxed 16:9, specify
+ * `16.0f / 9.0f` (aka `1.777777777f`) here.
+ */
+ LIBDRAGON_PREVIEW_SYM
+ float aspect_ratio;
+ /**
+ * @brief Add a margin to the display output to compensate for the TV overscan.
+ * @preview
+ *
+ * Leave 0 for emulators, upscaler or LCD TVs. Use #VI_CRT_MARGIN for
+ * adding some margin that will allow the picture to be fully visible on
+ * most TV CRTs.
+ *
+ * By default (when this value is 0), the framebuffer will be displayed at
+ * the maximum extents allowed by VI (not a physical maximum, but a good
+ * maximum that doesn't compromise compatibility of the video signal).
+ * This picture will be good for emulators, upscalers, or LCD TVs.
+ *
+ * On TV CRTs, instead, part of the picture will be displayed by the TV
+ * overscan. To compensate for this, you can reduce the picture size by this
+ * specified amount (expressed in percentage of the original picture).
+ * #VI_CRT_MARGIN (which is 0.05, aka 5%) is the suggested value you can
+ * use for this field
+ */
+ LIBDRAGON_PREVIEW_SYM
+ float overscan_margin;
} resolution_t;
///@cond
#define const static const /* fool doxygen to document these static members */
///@endcond
-/** @brief 256x240 mode */
-const resolution_t RESOLUTION_256x240 = {256, 240, false};
-/** @brief 320x240 mode */
-const resolution_t RESOLUTION_320x240 = {320, 240, false};
-/** @brief 512x240 mode, high-res progressive */
-const resolution_t RESOLUTION_512x240 = {512, 240, false};
-/** @brief 640x240 mode, high-res progressive */
-const resolution_t RESOLUTION_640x240 = {640, 240, false};
-/** @brief 512x480 mode, interlaced */
-const resolution_t RESOLUTION_512x480 = {512, 480, true};
-/** @brief 640x480 mode, interlaced */
-const resolution_t RESOLUTION_640x480 = {640, 480, true};
+
+/** Good default for a safe CRT overscan margin (5%) */
+#define VI_CRT_MARGIN 0.05f
+
+/** @brief 256x240 mode, stretched to 4:3, no borders */
+const resolution_t RESOLUTION_256x240 = {.width = 256, .height = 240, .interlaced = INTERLACE_OFF};
+/** @brief 320x240 mode, no borders */
+const resolution_t RESOLUTION_320x240 = {.width = 320, .height = 240, .interlaced = INTERLACE_OFF};
+/** @brief 512x240 mode, stretched to 4:3, no borders */
+const resolution_t RESOLUTION_512x240 = {.width = 512, .height = 240, .interlaced = INTERLACE_OFF};
+/** @brief 640x240 mode, stretched to 4:3, no borders */
+const resolution_t RESOLUTION_640x240 = {.width = 640, .height = 240, .interlaced = INTERLACE_OFF};
+/** @brief 512x480 mode, interlaced, stretched to 4:3, no borders */
+const resolution_t RESOLUTION_512x480 = {.width = 512, .height = 480, .interlaced = INTERLACE_HALF};
+/** @brief 640x480 mode, interlaced, no borders */
+const resolution_t RESOLUTION_640x480 = {.width = 640, .height = 480, .interlaced = INTERLACE_HALF};
+
#undef const
/** @brief Valid bit depths */
@@ -91,13 +193,22 @@ typedef enum
/** @brief Valid gamma correction settings */
typedef enum
{
- /** @brief Uncorrected gamma, should be used by default and with assets built by libdragon tools */
- GAMMA_NONE,
- /** @brief Corrected gamma, should be used on a 32-bit framebuffer
- * only when assets have been produced in linear color space and accurate blending is important */
- GAMMA_CORRECT,
+ /**
+ * @brief Uncorrected gamma.
+ *
+ * This is the default settings, and should be used with assets
+ * built by libdragon tools
+ */
+ GAMMA_NONE = 0,
+ /**
+ * @brief Corrected gamma.
+ *
+ * It should be used on a 32-bit framebuffer, only when assets have been
+ * produced in linear color space and accurate blending is important
+ */
+ GAMMA_CORRECT = VI_GAMMA_ENABLE,
/** @brief Corrected gamma with hardware dithered output */
- GAMMA_CORRECT_DITHER
+ GAMMA_CORRECT_DITHER = VI_GAMMA_DITHER_ENABLE,
} gamma_t;
/** @brief Valid display filter options.
@@ -115,8 +226,8 @@ typedef enum
/** @brief All display filters are disabled */
FILTERS_DISABLED,
/** @brief Resize the output image with a bilinear filter.
- * In general, VI is in charge of resizing the framebuffer to fit the TV resolution
- * (which is always NTSC 640x480 or PAL 640x512).
+ * In general, VI is in charge of resizing the framebuffer to fit the virtual
+ * TV resolution (which is always 640x480 on NTSC/MPAL, 640x576 on PAL).
* This option enables a bilinear interpolation that can be used during this resize. */
FILTERS_RESAMPLE,
/** @brief Reconstruct a 32-bit output from dithered 16-bit framebuffer. */
@@ -196,6 +307,43 @@ extern "C" {
*/
void display_init( resolution_t res, bitdepth_t bit, uint32_t num_buffers, gamma_t gamma, filter_options_t filters );
+/**
+ * @brief Change display parameters without reallocating framebuffers
+ * @preview
+ *
+ * Reapplies the same configuration options as #display_init but reuses the
+ * existing framebuffer memory. Use this to change resolution, bit depth,
+ * number of buffers, gamma or filters at run time without calling
+ * #display_close and #display_init.
+ *
+ * The display must already be initialized with #display_init. The new
+ * configuration is constrained: \p num_buffers must be less than or equal
+ * to the number of buffers originally allocated, and the new framebuffer
+ * size (width × height × bytes per pixel) must not exceed the originally
+ * allocated size. Multiple successive calls to #display_change are allowed,
+ * always within the memory initially allocated by #display_init.
+ *
+ * No allocation is performed: the same buffer pointers are reused. Buffers
+ * that were obtained via #display_get before the change complete their
+ * lifecycle with the old dimensions and are shown correctly; only buffers
+ * obtained via #display_get after the change use the new dimensions.
+ *
+ * Getters such as #display_get_width, #display_get_height and
+ * #display_get_bitdepth return the new dimensions immediately after
+ * #display_change returns.
+ *
+ * The actual VI register updates are applied lazily by the display subsystem
+ * after any pre-change queued frames have been shown.
+ *
+ * @param[in] res Requested resolution (same meaning as in #display_init)
+ * @param[in] bit Requested bit depth
+ * @param[in] num_buffers Number of buffers (must be ≤ originally allocated)
+ * @param[in] gamma Requested gamma setting
+ * @param[in] filters Requested display filtering options
+ */
+LIBDRAGON_PREVIEW_API
+void display_change( resolution_t res, bitdepth_t bit, uint32_t num_buffers, gamma_t gamma, filter_options_t filters );
+
/**
* @brief Close the display
*
@@ -245,6 +393,20 @@ surface_t* display_try_get(void);
*/
void display_show(surface_t* surf);
+/**
+ * @brief Return a memory surface that can be used as Z-buffer for the current
+ * @preview
+ * resolution
+ *
+ * This function lazily allocates and returns a surface that can be used
+ * as Z-buffer for the current resolution. The surface is automatically freed
+ * when the display is closed.
+ *
+ * @return The Z-buffer surface
+ */
+LIBDRAGON_PREVIEW_API
+surface_t* display_get_zbuf(void);
+
/**
* @brief Get the currently configured width of the display in pixels
*/
@@ -265,13 +427,65 @@ uint32_t display_get_bitdepth(void);
*/
uint32_t display_get_num_buffers(void);
+/**
+ * @brief Get the current refresh rate of the video output in Hz
+ * @preview
+ *
+ * The refresh rate is normally 50 for PAL and 60 for NTSC, but this function
+ * returns the hardware-accurate number which is close to those but not quite
+ * exact. Moreover, this will also account for advanced VI configurations
+ * affecting the refresh rate, like PAL60.
+ *
+ * @return Refresh rate in Hz (frames per second)
+ */
+LIBDRAGON_PREVIEW_API
+float display_get_refresh_rate(void);
+
/**
* @brief Get the current number of frames per second being rendered
*
- * @return float Frames per second
+ * @return Frames per second
*/
float display_get_fps(void);
+/**
+ * @brief Returns the "delta time", that is the time it took to the last frame
+ * @preview
+ * to be prepared and rendered.
+ *
+ * This function is useful for time-based animations and physics, as it allows
+ * to calculate the time elapsed between frames. Call this function once per
+ * frame to get the time elapsed since the last frame.
+ *
+ * @note Do not call this function more than once per frame. If needed, cache
+ * the result in a variable and use it multiple times.
+ *
+ * @return Time elapsed since the last complete frame (in seconds)
+ */
+LIBDRAGON_PREVIEW_API
+float display_get_delta_time(void);
+
+/**
+ * @brief Configure a limit for the frames per second
+ * @preview
+ *
+ * This function allows to set a limit for the frames per second to render.
+ * The limit is enforced by the display module, which will slow down calls
+ * to display_get() if need to respect the limit.
+ *
+ * Passing 0 as argument will disable the limit.
+ *
+ * @param fps The maximum number of frames per second to render (fractionals allowed)
+ */
+LIBDRAGON_PREVIEW_API
+void display_set_fps_limit(float fps);
+
+/**
+ * @brief Returns a surface that points to the framebuffer currently being shown on screen.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+surface_t display_get_current_framebuffer(void);
/** @cond */
__attribute__((deprecated("use display_get or display_try_get instead")))
diff --git a/include/dlfcn.h b/include/dlfcn.h
index 0dab0fcc3..465a580b0 100644
--- a/include/dlfcn.h
+++ b/include/dlfcn.h
@@ -41,40 +41,6 @@ typedef struct {
extern "C" {
#endif
-// Embedded GDB script to auto-load DSO symbols
-#ifndef N64_DSO
-/// @cond
-asm(
-".pushsection \".debug_gdb_scripts\", \"MS\",@progbits,1\n"
-".byte 4\n"
-".ascii \"gdb.inlined-script-dso-autoload\\n\"\n"
-".ascii \"import gdb\\n\"\n"
-".ascii \"class BreakpointDsoLoad(gdb.Breakpoint):\\n\"\n"
-".ascii \" def stop(self):\\n\"\n"
-".ascii \" frame = gdb.selected_frame()\\n\"\n"
-".ascii \" src_elf = gdb.execute('printf \\\"%s\\\", module->src_elf', False, True)\\n\"\n"
-".ascii \" prog_base = int(gdb.execute('printf \\\"%x\\\", module->prog_base', False, True), 16)\\n\"\n"
-".ascii \" print(\\\"Loading overlay: \\\", src_elf, \\\"(text:\\\", hex(prog_base), \\\")\\\")\\n\"\n"
-".ascii \" gdb.execute(\\\"add-symbol-file -readnow \\\" + src_elf + \\\" \\\" + hex(prog_base), False, True)\\n\"\n"
-".ascii \" return False\\n\"\n"
-".ascii \"class BreakpointDsoFree(gdb.Breakpoint):\\n\"\n"
-".ascii \" def stop(self):\\n\"\n"
-".ascii \" frame = gdb.selected_frame()\\n\"\n"
-".ascii \" src_elf = gdb.execute('printf \\\"%s\\\", module->src_elf', False, True)\\n\"\n"
-".ascii \" prog_base = int(gdb.execute('printf \\\"%x\\\", module->prog_base', False, True), 16)\\n\"\n"
-".ascii \" print(\\\"Unloading overlay: \\\", src_elf, \\\"(text:\\\", hex(prog_base), \\\")\\\")\\n\"\n"
-".ascii \" gdb.execute(\\\"remove-symbol-file -a \\\" + hex(prog_base), False, True)\\n\"\n"
-".ascii \" return False\\n\"\n"
-".ascii \"bp_load = BreakpointDsoLoad(\\\"__dl_insert_module\\\")\\n\"\n"
-".ascii \"bp_load.silent = True\\n\"\n"
-".ascii \"bl_free = BreakpointDsoFree(\\\"__dl_remove_module\\\")\\n\"\n"
-".ascii \"bl_free.silent = True\\n\"\n"
-".byte 0\n"
-".popsection\n"
-);
-/// @endcond
-#endif
-
/**
* @brief Open dynamic library
*
diff --git a/include/dma.h b/include/dma.h
index d6cd1b966..ac58ce852 100644
--- a/include/dma.h
+++ b/include/dma.h
@@ -8,8 +8,11 @@
#ifndef __LIBDRAGON_DMA_H
#define __LIBDRAGON_DMA_H
+
#include
#include
+#include "preview.h"
+#include "n64types.h"
/**
* @defgroup dma DMA Controller
@@ -29,6 +32,12 @@
* manipulating registers on a cartridge such as a gameshark. Code should never
* make raw 32-bit reads or writes in the cartridge domain as it could collide with
* an in-progress DMA transfer or run into caching issues.
+ *
+ * Async DMA requests are queued and executed one at a time, dequeued via the
+ * PI interrupt. Each async function returns a ticket that can be used with
+ * #dma_get_progress and #dma_finished to query the transfer,
+ * #dma_wait_started or #dma_wait_finished to wait for that transfer, or
+ * #dma_wait to wait for all queued transfers to finish.
* @{
*/
@@ -36,12 +45,37 @@
extern "C" {
#endif
+/**
+ * @name PI Register Definitions
+ * @{
+ */
#define PI_DRAM_ADDR ((volatile uint32_t*)0xA4600000) ///< PI DMA: DRAM address register
#define PI_CART_ADDR ((volatile uint32_t*)0xA4600004) ///< PI DMA: cartridge address register
#define PI_RD_LEN ((volatile uint32_t*)0xA4600008) ///< PI DMA: read length register
#define PI_WR_LEN ((volatile uint32_t*)0xA460000C) ///< PI DMA: write length register
#define PI_STATUS ((volatile uint32_t*)0xA4600010) ///< PI: status register
+#define PI_STATUS_DMA_BUSY (1 << 0) ///< PI status: DMA is in progress
+#define PI_STATUS_IO_BUSY (1 << 1) ///< PI status: IO is in progress
+#define PI_STATUS_ERROR (1 << 2) ///< PI status: error occurred
+/** @} */
+
+/**
+ * @brief PI BSD domain bus timing parameters.
+ *
+ * These map directly onto a PI domain's LAT/PWD/PGS/RLS registers
+ * (PI_BSD_DOM1_* for cartridge ROM, PI_BSD_DOM2_* for SRAM/FlashRAM), which
+ * configure the bus timing the PI uses to talk to a peripheral in that domain.
+ * Each field is the raw register value.
+ */
+typedef struct
+{
+ uint8_t latency; ///< PI_BSD_DOMx_LAT: bus latency.
+ uint8_t pulse_width; ///< PI_BSD_DOMx_PWD: pulse width.
+ uint8_t page_size; ///< PI_BSD_DOMx_PGS: page size; DMA auto-split boundary is 2^(page_size+2) bytes.
+ uint8_t release; ///< PI_BSD_DOMx_RLS: release duration.
+} pi_dom_timings_t;
+
/**
* @brief Start writing data to a peripheral through PI DMA (low-level)
*
@@ -50,17 +84,20 @@ extern "C" {
* well-defined only for RAM addresses which are multiple of 8, ROM addresses
* which are multiple of 2, and lengths which are multiple of 2.
*
- * Use #dma_wait to wait for the end of the transfer.
- *
+ * Use #dma_wait_finished to wait for the end of the transfer, or #dma_finished
+ * to check it without blocking.
*
* @param[out] ram_address
* Pointer to a buffer to read data from (must be 8-byte aligned)
* @param[in] pi_address
* Memory address of the peripheral to write to (must be 2-byte aligned)
* @param[in] len
- * Length in bytes to write into pi_address (must be multiple of 2)
+ * Length in bytes to write into pi_address (must be a multiple of
+ * 2 and no greater than 16 MiB)
+ *
+ * @return Ticket identifying the transfer (always not 0)
*/
-void dma_write_raw_async(const void *ram_address, unsigned long pi_address, unsigned long len);
+uint64_t dma_write_raw_async(const void *ram_address, pi_addr_t pi_address, unsigned long len);
/**
* @brief Write to a peripheral
@@ -80,7 +117,7 @@ void dma_write_raw_async(const void *ram_address, unsigned long pi_address, unsi
* to access the whole range.
* If you need to read outside the ROM area, use #dma_write_raw_async instead.
*/
-void dma_write(const void * ram_address, unsigned long pi_address, unsigned long len);
+void dma_write(const void * ram_address, pi_addr_t pi_address, unsigned long len);
/**
@@ -91,7 +128,8 @@ void dma_write(const void * ram_address, unsigned long pi_address, unsigned long
* well-defined only for RAM addresses which are multiple of 8, ROM addresses
* which are multiple of 2, and lengths which are multiple of 2.
*
- * Use #dma_wait to wait for the end of the transfer.
+ * Use #dma_wait_finished to wait for the end of the transfer, or #dma_finished
+ * to check it without blocking.
*
* See #dma_read_async for a higher level primitive which can perform almost
* arbitrary transfers.
@@ -101,9 +139,12 @@ void dma_write(const void * ram_address, unsigned long pi_address, unsigned long
* @param[in] pi_address
* Memory address of the peripheral to read from (must be 2-byte aligned)
* @param[in] len
- * Length in bytes to read into ram_address (must be multiple of 2)
+ * Length in bytes to read into ram_address (must be a multiple of
+ * 2 and no greater than 16 MiB)
+ *
+ * @return Ticket identifying the transfer (always not 0)
*/
-void dma_read_raw_async(void *ram_address, unsigned long pi_address, unsigned long len);
+uint64_t dma_read_raw_async(void *ram_address, pi_addr_t pi_address, unsigned long len);
/**
* @brief Start reading data from a peripheral through PI DMA
@@ -119,7 +160,9 @@ void dma_read_raw_async(void *ram_address, unsigned long pi_address, unsigned lo
* odd addresses. Notice that this function will assert if this constraint is
* not respected.
*
- * Use #dma_wait to wait for the end of the transfer.
+ * Use #dma_wait_finished to wait for the end of the transfer, or #dma_finished
+ * to check it without blocking.
+ * The buffer is fully valid only after completion has been confirmed.
*
* For non performance sensitive tasks such as reading and parsing data from
* ROM at loading time, a better option is to use DragonFS, where #dfs_read
@@ -130,9 +173,11 @@ void dma_read_raw_async(void *ram_address, unsigned long pi_address, unsigned lo
* @param[in] pi_address
* Memory address of the peripheral to read from
* @param[in] len
- * Length in bytes to read into ram_pointer
+ * Length in bytes to read into ram_pointer (no greater than 16 MiB)
+ *
+ * @return Ticket identifying the transfer (always not 0)
*/
-void dma_read_async(void *ram_pointer, unsigned long pi_address, unsigned long len);
+uint64_t dma_read_async(void *ram_pointer, pi_addr_t pi_address, unsigned long len);
/**
* @brief Read data from a peripheral through PI DMA, waiting for completion.
@@ -152,20 +197,72 @@ void dma_read_async(void *ram_pointer, unsigned long pi_address, unsigned long l
* to access the whole range.
* If you need to read outside the ROM area, use #dma_read_async instead.
*/
-void dma_read(void * ram_address, unsigned long pi_address, unsigned long len);
+void dma_read(void * ram_address, pi_addr_t pi_address, unsigned long len);
/**
- * @brief Wait until an async DMA or I/O transfer is finished.
+ * @brief Wait until all queued async DMA transfers are finished.
*/
void dma_wait(void);
+/**
+ * @brief Wait until an async DMA transfer has started.
+ * @preview
+ *
+ * If the kernel is running, the calling thread yields while the transfer is
+ * still queued. A transfer which has already completed is considered started.
+ *
+ * @param ticket Ticket returned by an async DMA function
+ */
+LIBDRAGON_PREVIEW_API
+void dma_wait_started(uint64_t ticket);
+
+/**
+ * @brief Wait until an async DMA transfer is finished.
+ * @preview
+ *
+ * If the kernel is running, the calling thread yields while waiting.
+ *
+ * @param ticket Ticket returned by an async DMA function
+ */
+LIBDRAGON_PREVIEW_API
+void dma_wait_finished(uint64_t ticket);
+
+/**
+ * @brief Return the progress of an async DMA transfer.
+ * @preview
+ *
+ * If the transfer has not started yet, this function returns zero.
+ *
+ * If it is currently running, the returned value identifies the offset of
+ * the next byte that will be transferred by the DMA, so all preceding bytes
+ * are safe to consume (when doing a DMA-racing approach).
+ *
+ * If the transfer has completed, it returns the total requested length. Notice
+ * that at this point, another transfer might already have started.
+ *
+ * @param ticket Ticket returned by an async DMA function
+ * @return Number of bytes already transferred
+ */
+LIBDRAGON_PREVIEW_API
+unsigned long dma_get_progress(uint64_t ticket);
+
+/**
+ * @brief Check whether an async DMA transfer is finished.
+ * @preview
+ *
+ * @param ticket Ticket returned by an async DMA function
+ * @return True if the transfer is fully complete
+ */
+LIBDRAGON_PREVIEW_API
+bool dma_finished(uint64_t ticket);
+
/**
- * @brief Read a 32 bit integer from a peripheral using the CPU.
+ * @brief Read a 32 bit integer from a peripheral on the PI bus using the CPU.
*
* @param[in] pi_address
- * Memory address of the peripheral to read from
+ * PI address of the peripheral to read from
*
* @return The 32 bit value read from the peripheral
*
@@ -174,17 +271,17 @@ void dma_wait(void);
*
* @see #io_accessible
*/
-uint32_t io_read(uint32_t pi_address);
+uint32_t io_read(pi_addr_t pi_address);
/**
- * @brief Write a 32 bit integer to a peripheral using the CPU.
+ * @brief Write a 32 bit integer to a peripheral on the PI bus using the CPU.
*
* Notice that writes are performed asynchronously, so the data might have not been
* fully written to the peripheral yet when the function returns. Use #dma_wait if
* you need to wait for the transfer to be finished.
*
* @param[in] pi_address
- * Memory address of the peripheral to write to
+ * PI address of the peripheral to write to
* @param[in] data
* 32 bit value to write to peripheral
*
@@ -193,7 +290,7 @@ uint32_t io_read(uint32_t pi_address);
*
* @see #io_accessible
*/
-void io_write(uint32_t pi_address, uint32_t data);
+void io_write(pi_addr_t pi_address, uint32_t data);
/**
* @brief Check whether the specified PI address can be accessed doing I/O from CPU
@@ -206,25 +303,20 @@ void io_write(uint32_t pi_address, uint32_t data);
*
* * 0x0500_0000 - 0x0FFF_FFFF: used by N64DD and SRAM on cartridge
* * 0x1000_0000 - 0x1FBF_FFFF: cartridge ROM
- * * 0x1FD0_0000 - 0x1FFF_FFFF: no known PI peripherals use this
+ * * 0x1FD0_0000 - 0x7FFF_FFFF: no known PI peripherals use this
*
* The rest of the 32-bit address range is only accessible via DMA.
*
- * Notice also that the range 0x2000_0000 - 0x7FFF_FFFF is theoretically accessible
- * by the CPU but only via 64-bit addressing, so it requires assembly instructions
- * (as the libdragon toolchain uses 32-bit pointers). No known PI peripherals use this
- * range anyway.
- *
* This function checks whether the specified address falls into the range accessible
* via CPU or not.
*
* @param pi_address PI address to check
* @return True if the address is memory mapped, false if it is not
*/
-bool io_accessible(uint32_t pi_address);
+bool io_accessible(pi_addr_t pi_address);
__attribute__((deprecated("use dma_wait instead")))
-volatile int dma_busy(void);
+int dma_busy(void);
#ifdef __cplusplus
diff --git a/include/dragonfs.h b/include/dragonfs.h
index 0ed87c128..4c7e9a1d9 100644
--- a/include/dragonfs.h
+++ b/include/dragonfs.h
@@ -47,10 +47,10 @@
* @{
*/
-#ifdef N64
+#include "n64types.h"
+#ifdef N64
#include "ioctl.h"
-
#endif
/**
@@ -74,7 +74,27 @@
#define MAX_DIRECTORY_DEPTH 100
/**
- * @brief Base ROM Address Request ioctl Command Code
+ * @brief Return the PI address of an open DragonFS file
+ *
+ * DragonFS files are stored contiguously in ROM, so this ioctl returns the
+ * PI address of the first byte of the file. The address does not depend on
+ * the current seek position; add the result of lseek(fd, 0, SEEK_CUR) to
+ * obtain the address of the current offset.
+ *
+ * The address is written to a #pi_addr_t pointed to by the ioctl argument.
+ * Direct access to ROM data must go through #io_read or #dma_read. The
+ * address is guaranteed to be 2-byte aligned, but not necessarily 4-byte
+ * aligned, so #io_read should be used with care.
+ *
+ * This is the ioctl equivalent of #dfs_rom_addr for an already-open file.
+ *
+ * \code{.c}
+ * FILE *f = fopen("rom:/myfile.dat", "rb");
+ * pi_addr_t rom_addr = 0;
+ * ioctl(fileno(f), IODFS_GET_ROM_BASE, &rom_addr);
+ * \endcode
+ *
+ * @see #dfs_rom_addr
*/
#define IODFS_GET_ROM_BASE _IO('D', 0)
@@ -145,17 +165,17 @@ extern "C" {
* rompak_internal.h). Most users should use this option.
*
* Otherwise, if the ROM cannot be built with a rompak TOC for some reason,
- * a virtual address should be passed. This is normally 0xB0000000 + the offset
- * used when building your ROM + the size of the header file used (typically 0x1000).
+ * a PI address should be passed. This is normally 0x10000000 + the offset
+ * at which the DFS image is located within the ROM.
*
* @param[in] base_fs_loc
- * Virtual address in cartridge space at which to find the filesystem, or
+ * PI address in cartridge space at which to find the filesystem, or
* DFS_DEFAULT_LOCATION to automatically search for the filesystem in the
* cartridge (using the rompak).
*
* @return DFS_ESUCCESS on success or a negative error otherwise.
*/
-int dfs_init(uint32_t base_fs_loc);
+int dfs_init(pi_addr_t base_fs_loc);
/**
@@ -247,25 +267,28 @@ int dfs_eof(uint32_t handle);
int dfs_size(uint32_t handle);
/**
- * @brief Return the physical address of a file (in ROM space)
+ * @brief Return the PI address of a file (in ROM space)
*
* This function should be used for highly-specialized, high-performance
- * use cases. Using dfs_open / dfs_read is generally acceptable
+ * use cases. Using open / fopen / read / fread is generally acceptable
* performance-wise, and is easier to use rather than managing
* direct access to PI space.
*
- * Direct access to ROM data must go through io_read or dma_read. Do not
- * dereference directly as the console might hang if the PI is busy.
+ * Direct access to ROM data must go through #io_read or #dma_read. Notice
+ * that the address is guaranteed to be 2-byte aligned, but not necessarily
+ * 4-byte aligned, so #io_read should be used with care.
+ *
+ * To obtain the address of an already-open file, use #ioctl with IODFS_GET_ROM_BASE.
*
* @param[in] path
* Name of the file
*
- * @return A pointer to the physical address of the file body, or 0
- * if the file was not found.
+ * @return The PI address of the file body, or 0 if the file was not found.
*
* @see #dfs_rom_size
+ * @see #IODFS_GET_ROM_BASE
*/
-uint32_t dfs_rom_addr(const char *path);
+pi_addr_t dfs_rom_addr(const char *path);
/**
* @brief Return the size of a file (in ROM data)
diff --git a/include/eeprom.h b/include/eeprom.h
index 8b8f1d1b1..1f95cc409 100644
--- a/include/eeprom.h
+++ b/include/eeprom.h
@@ -10,6 +10,7 @@
#ifndef __LIBDRAGON_EEPROM_H
#define __LIBDRAGON_EEPROM_H
+
/**
* @defgroup peripherals Peripherals subsystem
* @ingroup libdragon
@@ -30,9 +31,47 @@
* * A higher-level API (eepromfs.h) for higher-level access to EEPROM
* with structured data.
*
+ * The low-level API exposes:
+ * - #eeprom_present and #eeprom_total_blocks to probe EEPROM capacity
+ * - #eeprom_read and #eeprom_read_bytes to read save data
+ * - #eeprom_write and #eeprom_write_bytes to update save data
+ * - #eeprom_is_busy to query whether background flush is still pending
+ *
+ * Current implementation uses a RAM master-copy with background write-back:
+ * reads are served from RAM cache (with lazy block fetch from EEPROM),
+ * writes update RAM immediately and are persisted asynchronously.
+ * This implies eventual consistency with physical EEPROM.
+ *
+ * Since writes happen in background, make sure to provide user feedback
+ * while writes are happening, checking with #eeprom_is_busy and telling
+ * the user that a write is in progress.
+ *
+ * Shutting down the console while a write is in progress can result in
+ * the block being corrupted. EEPFS (eepromfs.h) provides higher-level
+ * filesystem integrity features to mitigate this.
+ *
+ * Activating EEPROM in emulators or flashcarts
+ * --------------------------------------------
+ * Libdragon offers support to advertise the need of an EEPROM to emulators
+ * and flashcarts. To do so, add N64_ROM_SAVETYPE=eeprom4k or
+ * N64_ROM_SAVETYPE=eeprom16k to your Makefile.
+ *
+ * This uses the advanced homebrew header which is supported by most emulators
+ * and flashcarts compatible with libdragon. It still advised to use the
+ * #eeprom_present and #eeprom_total_blocks functions to probe the EEPROM
+ * and gracefully handles the case where no EEPROM is present, even if it
+ * was requested.
+ *
+ * Cheap flashcarts might not support the advanced homebrew header,
+ * and they most likely default to a 4k EEPROM as default save states for
+ * non-commercial ROMs. If your ROM requires a 16k EEPROM to operate
+ * properly, users of those flashcarts will need to manually configure the
+ * EEPROM type using flashcart-specific instructions.
*/
+#include
#include
+#include "preview.h"
/**
* @brief EEPROM Probe Values
@@ -78,26 +117,28 @@ size_t eeprom_total_blocks( void );
/**
* @brief Read a block from EEPROM.
*
- * This operation will wait for the EEPROM busy bit to clear before reading;
- * you may want to pause audio before calling this to prevent stuttering.
- *
- * This operation is quite fast (compared to writes) as it takes approximately
- * 750 µs.
+ * This function will read a block of data from EEPROM (8 bytes). Most users
+ * will want to use #eeprom_read_bytes instead, which is more flexible.
*
* @param[in] block
* Block to read data from. Joybus accesses EEPROM in 8-byte blocks.
*
* @param[out] dest
* Destination buffer for the eight bytes read from EEPROM.
+ *
+ * @see #eeprom_read_bytes
*/
-void eeprom_read( uint8_t block, uint8_t * dest );
+void eeprom_read( uint8_t block, void * dest );
/**
* @brief Write a block to EEPROM.
*
- * Once a block is written, the EEPROM will be busy for up to 6 milliseconds.
- * This operation will wait for the EEPROM busy bit to clear before writing;
- * you may want to pause audio before calling this to prevent stuttering.
+ * This function writes a block of data to EEPROM (8 bytes). Most users
+ * will want to use #eeprom_write_bytes instead, which is more flexible.
+ *
+ * @note Writes are eventually consistent to the EEPROM, so they will be
+ * persisted in background to the actual EEPROM. The written data is
+ * immediately visible to read APIs though.
*
* @param[in] block
* Block to write data to. Joybus accesses EEPROM in 8-byte blocks.
@@ -107,13 +148,15 @@ void eeprom_read( uint8_t block, uint8_t * dest );
*
* @return the EEPROM status byte
*/
-uint8_t eeprom_write( uint8_t block, const uint8_t * src );
+uint8_t eeprom_write( uint8_t block, const void * src );
/**
* @brief Read a buffer of bytes from EEPROM.
*
- * This is a high-level convenience helper that abstracts away the
- * one-at-a-time EEPROM block access pattern.
+ * Read an arbitrary amount of data from the EEPROM. Normally reads are quite
+ * fast, even more so if the internal cache is already populated. In general
+ * you should be able to use reads without long stalls that might affect
+ * graphics or audio.
*
* @param[out] dest
* Destination buffer to read data into
@@ -122,21 +165,26 @@ uint8_t eeprom_write( uint8_t block, const uint8_t * src );
* @param[in] len
* Byte length of data to read into buffer
*/
-void eeprom_read_bytes( uint8_t * dest, size_t start, size_t len );
+void eeprom_read_bytes( void * dest, size_t start, size_t len );
/**
* @brief Write a buffer of bytes to EEPROM.
*
- * This is a high-level convenience helper that abstracts away the
- * one-at-a-time EEPROM block access pattern.
+ * Writes an arbitrary amount of data to the EEPROM. Notice that writes are
+ * eventually consistent to the EEPROM: this function will return immediately,
+ * but the data will be persisted in background to the actual EEPROM.
+ *
+ * The background persistence is quite slow. These are measured times for
+ * writing the whole EEPROM:
*
- * Each EEPROM block write takes approximately 6 milliseconds;
- * this operation may block for a while with large buffer sizes:
+ * * 4k EEPROM: 64 blocks * 6ms = 384ms
+ * * 16k EEPROM: 256 blocks * 6ms = 1536ms
*
- * * 4k EEPROM: 64 blocks * 6ms = 384ms!
- * * 16k EEPROM: 256 blocks * 6ms = 1536ms!
+ * Make sure the user is aware that a write is in progress and do not turn off
+ * the console, otherwise the EEPROM contents might get corrupted. You may want
+ * to display a "save in progress" message or indicator on the screen.
*
- * You may want to pause audio before calling this.
+ * You can use #eeprom_is_busy to check if a write is in progress at any point.
*
* @param[in] src
* Source buffer containing data to write
@@ -146,8 +194,43 @@ void eeprom_read_bytes( uint8_t * dest, size_t start, size_t len );
*
* @param[in] len
* Byte length of the src buffer
+ *
+ * @see #eeprom_is_busy
+ */
+void eeprom_write_bytes( const void * src, size_t start, size_t len );
+
+/**
+ * @brief Return whether EEPROM flush is currently running.
+ * @preview
+ *
+ * This function reports whether the background flusher is actively persisting
+ * data to EEPROM.
+ *
+ * Typical usage is to continue gameplay/UI while periodically checking this,
+ * and show feedback such as "saving..." until it returns false.
+ *
+ * @return true if background flush is currently active, false otherwise.
+ */
+LIBDRAGON_PREVIEW_API
+bool eeprom_is_busy(void);
+
+
+/**
+ * @brief Wait until the EEPROM is completely idle.
+ * @preview
+ *
+ * This function will block until the EEPROM is completely idle, i.e. until
+ * all background writes have completed.
+ *
+ * This is similar to making a loop around #eeprom_is_busy, but it allows
+ * to switch to other threads while waiting.
+ *
+ * @note EEPROM writes are quite slow, so this function can block for a
+ * long time, up to hundreds of milliseconds. Therefore it should be
+ * used only when there is no graphics or audio to process.
*/
-void eeprom_write_bytes( const uint8_t * src, size_t start, size_t len );
+LIBDRAGON_PREVIEW_API
+void eeprom_wait_idle(void);
#ifdef __cplusplus
}
diff --git a/include/eepromfs.h b/include/eepromfs.h
index 0b704be0a..b4d257fc6 100644
--- a/include/eepromfs.h
+++ b/include/eepromfs.h
@@ -8,9 +8,11 @@
#ifndef __LIBDRAGON_EEPROMFS_H
#define __LIBDRAGON_EEPROMFS_H
+
#include
#include
#include
+#include "preview.h"
/**
* @name EEPROM filesystem return values
@@ -22,7 +24,7 @@
#define EEPFS_EBADINPUT -1
/** @brief File does not exist */
#define EEPFS_ENOFILE -2
-/** @brief Bad filesystem */
+/** @brief Filesystem is too big for the EEPROM */
#define EEPFS_EBADFS -3
/** @brief No memory for operation */
#define EEPFS_ENOMEM -4
@@ -30,6 +32,8 @@
#define EEPFS_EBADHANDLE -5
/** @brief Filesystem already initialized */
#define EEPFS_ECONFLICT -6
+/** @brief File is corrupted (checksum mismatch) */
+#define EEPFS_CORRUPTED -7
/** @} */
#ifdef __cplusplus
@@ -53,7 +57,7 @@ typedef struct eepfs_entry_t
*
* A leading '/' is optional and will be ignored if set.
*
- * The filesytem does not support entries for directories,
+ * The filesystem does not support entries for directories,
* nor does it support listing files in a given directory.
*/
const char * path;
@@ -73,6 +77,34 @@ typedef struct eepfs_entry_t
* * 16k EEPROM: 2048 - 8 = 2040 bytes (255 blocks) free.
*/
size_t size;
+ /**
+ * @brief Automatically store and verify a checksum of the file
+ * @preview
+ *
+ * When this flag is set, eepfs will calculate a 16-bit checksum
+ * of the file and append it to the file itself. The checksum
+ * will also be automatically verified when the file is read.
+ */
+ LIBDRAGON_PREVIEW_SYM
+ bool checksum;
+ /**
+ * @brief Keep a backup copy of the file in the EEPROM.
+ * @preview
+ *
+ * When this flag is set, eepfs will automatically keep a backup
+ * copy of the file in the EEPROM. The backup copy is automatically
+ * retrieved if the main copy is corrupted (use of checksum is strongly
+ * recommended if backup is enabled).
+ *
+ * This is useful to prevent data loss in case of corruption or
+ * power failure.
+ *
+ * In addition to the backup copy, the filesystem will also need
+ * to store 2 extra bytes (1 per copy) to keep a reference to the
+ * newest copy of the file.
+ */
+ LIBDRAGON_PREVIEW_SYM
+ bool backup;
} eepfs_entry_t;
/**
@@ -136,8 +168,10 @@ int eepfs_read(const char * path, void * dest, size_t size);
/**
* @brief Writes an entire file to the EEPROM filesystem.
*
- * Each EEPROM block write takes approximately 15 milliseconds;
- * this operation may block for a while!
+ * @note Writes are eventually consistent, so they will be
+ * performed in background and may take a while to complete.
+ * Use #eeprom_is_busy or #eeprom_wait_idle to check the status
+ * of the write.
*
* @param[in] path
* Path of file in EEPROM filesystem to write to
@@ -157,10 +191,12 @@ int eepfs_write(const char * path, const void * src, size_t size);
* All files in the filesystem must always exist at the size specified
* during #eepfs_init
*
- * Each EEPROM block write takes approximately 15 milliseconds;
- * this operation may block for a while!
- *
* Be advised: this is a destructive operation that cannot be undone!
+ *
+ * @note Writes are eventually consistent, so they will be
+ * performed in background and may take a while to complete.
+ * Use #eeprom_is_busy or #eeprom_wait_idle to check the status
+ * of the write.
*
* @retval EEPFS_ESUCCESS if successful
* @retval EEPFS_ENOFILE if the path is not a valid file
@@ -186,7 +222,6 @@ int eepfs_erase(const char * path);
* filesystem expects. If not, the best move is to erase everything
* and start from zero.
*
- * @see eepfs_generate_signature
* @see #eepfs_wipe
*
* @retval true if the signature in EEPROM matches the filesystem signature
@@ -199,9 +234,6 @@ bool eepfs_verify_signature(void);
*
* This is useful when you want to erase all files in the filesystem.
*
- * Each EEPROM block write takes approximately 15 milliseconds;
- * this operation may block for a while:
- *
* * 4k EEPROM: 64 blocks * 15ms = 960ms!
* * 16k EEPROM: 256 blocks * 15ms = 3840ms!
*
@@ -209,6 +241,11 @@ bool eepfs_verify_signature(void);
*
* Be advised: this is a destructive operation that cannot be undone!
*
+ * @note Writes are eventually consistent, so they will be
+ * performed in background and may take a while to complete.
+ * Use #eeprom_is_busy or #eeprom_wait_idle to check the status
+ * of the write.
+ *
* @see #eepfs_verify_signature
*/
void eepfs_wipe(void);
diff --git a/include/eia608.h b/include/eia608.h
new file mode 100644
index 000000000..894f5bf8f
--- /dev/null
+++ b/include/eia608.h
@@ -0,0 +1,273 @@
+/**
+ * @file eia608.h
+ * @author Giovanni Bajo
+ * @brief Library to generate NTSC EIA-608 closed captions
+ * @ingroup display
+ *
+ * This module provides a simple way to generate EIA-608 closed captions
+ * for NTSC video signals. The EIA-608 standard defines a way to transmit
+ * text data over the vertical blanking interval of an NTSC video signal.
+ *
+ * The N64 VI is powerful enough that it can be programmed to emit a
+ * EIA-608 compliant signal, while still displaying a framebuffer. This module
+ * offers a simple API to implement that.
+ *
+ * To generate caption, first initialize the module at boot via #eia608_init.
+ *
+ * Once the video mode has been fully configured via either #display_init
+ * or via the VI library, you can start generating captions by calling
+ * #eia608_start. This function will perform calculations to inject the
+ * EIA-608 signal into the video signal, so it is important to call it
+ * after all VI configurations have been performed. If you need to perform
+ * *any* change to VI configuration, make sure to call #eia608_stop first,
+ * and then call #eia608_start again.
+ *
+ * To generate captions, you can use #eia608_caption_prepare and #eia608_caption_show
+ * to display a UTF-8 string on the screen. These functions will take care of
+ * encoding the string into EIA-608 format, doing some basic formatting (word-wrap,
+ * and centering), and hiding it after the specified number of seconds.
+ *
+ * This module also provides a way to display raw EIA-608 data, via
+ * #eia608_write_raw. One possible use case for this function is to
+ * display captions saved into the Scenarist SCC format, which is a very
+ * simple text format that encodes captions as sequence of raw 16-bit
+ * EIA-608 words.
+ *
+ * @note The runtime performance impact of this module is around ~100 µs per
+ * VI frame (so 60 Hz), irrespective of the actual game speed.
+ *
+ * @note EIA-608 is a standard for NTSC video signals. It is not compatible
+ * with PAL or MPAL TVs.
+ *
+ * @see Wikipedia article on EIA-608: https://en.wikipedia.org/wiki/EIA-608
+ * @see EIA-608 specs: https://www.govinfo.gov/content/pkg/CFR-2007-title47-vol1/pdf/CFR-2007-title47-vol1-sec15-119.pdf
+ */
+
+#ifndef LIBDRAGON_EIA608_H
+#define LIBDRAGON_EIA608_H
+
+#include
+#include
+#include "preview.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** @brief Caption channel */
+typedef enum {
+ EIA608_CC1 = 1,
+ EIA608_CC2 = 2,
+} eia608_channel_t;
+
+/** @brief Caption display parameters */
+typedef struct {
+ int row; ///< The row to display the caption on, range 1-15 (default: 11)
+ bool underline; ///< Enable underline for the caption (default: false)
+} eia608_captionparms_t;
+
+/** @brief Calculate EIA608 parity for 7-bit value */
+#define EIA608_PARITY(b) ((((1 ^ ((b)>>0) ^ ((b)>>1) ^ ((b)>>2) ^ ((b)>>3) ^ ((b)>>4) ^ ((b)>>5) ^ ((b)>>6)) & 1) << 7) | (b))
+
+///@cond
+#define EIA608_CTRL(c) ((EIA608_PARITY((c) & 0xFF)) | (EIA608_PARITY((c) >> 8) << 8))
+///@endcond
+
+/**
+ * @brief EIA-608 control words
+ *
+ * This is a list of EIA-608 control words as defined by the spec. Notice
+ * that the semantic of these control words is a bit complex, so make sure
+ * to refer to the spec for more information.
+ */
+typedef enum {
+ EIA608_NOP = EIA608_CTRL(0x0000), // No operation
+ EAI608_CC1_TS = EIA608_CTRL(0x1139), // CC1: Transparent space
+ EAI608_CC2_TS = EIA608_CTRL(0x1939), // CC2: Transparent space
+
+ EIA608_CC1_RCL = EIA608_CTRL(0x1420), // CC1: Resume caption loading
+ EIA608_CC2_RCL = EIA608_CTRL(0x1C20), // CC2: Resume caption loading
+ EIA608_CC1_BS = EIA608_CTRL(0x1421), // CC1: Backspace
+ EIA608_CC2_BS = EIA608_CTRL(0x1C21), // CC2: Backspace
+ EIA608_CC1_AOF = EIA608_CTRL(0x1422), // CC1: Alarm off
+ EIA608_CC2_AOF = EIA608_CTRL(0x1C22), // CC2: Alarm off
+ EIA608_CC1_AON = EIA608_CTRL(0x1423), // CC1: Alarm on
+ EIA608_CC2_AON = EIA608_CTRL(0x1C23), // CC2: Alarm on
+ EIA608_CC1_DER = EIA608_CTRL(0x1424), // CC1: Delete to end of row
+ EIA608_CC2_DER = EIA608_CTRL(0x1C24), // CC1: Delete to end of row
+ EIA608_CC1_RU2 = EIA608_CTRL(0x1425), // CC1: Roll-up 2 rows
+ EIA608_CC2_RU2 = EIA608_CTRL(0x1C25), // CC2: Roll-up 2 rows
+ EIA608_CC1_RU3 = EIA608_CTRL(0x1426), // CC1: Roll-up 3 rows
+ EIA608_CC2_RU3 = EIA608_CTRL(0x1C26), // CC2: Roll-up 3 rows
+ EIA608_CC1_RU4 = EIA608_CTRL(0x1427), // CC1: Roll-up 4 rows
+ EIA608_CC2_RU4 = EIA608_CTRL(0x1C27), // CC2: Roll-up 4 rows
+ EIA608_CC1_FON = EIA608_CTRL(0x1428), // CC1: Flash on
+ EIA608_CC2_FON = EIA608_CTRL(0x1C28), // CC2: Flash on
+ EIA608_CC1_RDC = EIA608_CTRL(0x1429), // CC1: Resume direct captioning
+ EIA608_CC2_RDC = EIA608_CTRL(0x1C29), // CC2: Resume direct captioning
+ EIA608_CC1_TR = EIA608_CTRL(0x142A), // CC1: Text restart
+ EIA608_CC2_TR = EIA608_CTRL(0x1C2A), // CC2: Text restart
+ EIA608_CC1_RTD = EIA608_CTRL(0x142B), // CC1: Resume text display
+ EIA608_CC2_RTD = EIA608_CTRL(0x1C2B), // CC2: Resume text display
+ EIA608_CC1_EDM = EIA608_CTRL(0x142C), // CC1: Erase displayed memory
+ EIA608_CC2_EDM = EIA608_CTRL(0x1C2C), // CC2: Erase displayed memory
+ EIA608_CC1_CR = EIA608_CTRL(0x142D), // CC1: Carriage return
+ EIA608_CC2_CR = EIA608_CTRL(0x1C2D), // CC2: Carriage return
+ EIA608_CC1_ENM = EIA608_CTRL(0x142E), // CC1: Erase non-displayed memory
+ EIA608_CC2_ENM = EIA608_CTRL(0x1C2E), // CC2: Erase non-displayed memory
+ EIA608_CC1_EOC = EIA608_CTRL(0x142F), // CC1: End of caption
+ EIA608_CC2_EOC = EIA608_CTRL(0x1C2F), // CC2: End of caption
+ EIA608_CC1_TO1 = EIA608_CTRL(0x1721), // CC1: Tab offset 1
+ EIA608_CC2_TO1 = EIA608_CTRL(0x1F21), // CC2: Tab offset 1
+ EIA608_CC1_TO2 = EIA608_CTRL(0x1722), // CC1: Tab offset 2
+ EIA608_CC2_TO2 = EIA608_CTRL(0x1F22), // CC2: Tab offset 2
+ EIA608_CC1_TO3 = EIA608_CTRL(0x1723), // CC1: Tab offset 3
+ EIA608_CC2_TO3 = EIA608_CTRL(0x1F23), // CC2: Tab offset 3
+} eia608_ctrl_t;
+
+/**
+ * @brief Initialize the eia608 library
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+void eia608_init(void);
+
+/**
+ * @brief Deinitialize the eia608 library
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+void eia608_close(void);
+
+/**
+ * @brief Start emitting a EIA-608 compliant signal on the video output.
+ * @preview
+ *
+ * This function configures the module to start emitting EIA-608 on the current
+ * configured resolution, and starts the signal generation.
+ *
+ * Notice that internal parameters will be configured depending on the current
+ * video mode, so make sure to call this function after the video mode has been
+ * set (eg: after #display_init).
+ *
+ * If you need to do *any* changes to the VI configuration, make sure to stop
+ * the EIA-608 signal first by calling #eia608_stop.
+ *
+ * @note TVs might require up to one second to lock both the NTSC signal *and*
+ * the EIA-608 signal. Make sure to call this function at least one second
+ * before you need the first caption to be displayed.
+ *
+ * @see #eia608_stop
+ */
+LIBDRAGON_PREVIEW_API
+void eia608_start(void);
+
+/**
+ * @brief Stop emitting a EIA-608 compliant signal on the video output.
+ * @preview
+ *
+ * This function stops the EIA-608 signal generation, and resets the internal
+ * state of the module. It should be called before making any changes to the
+ * VI configuration, and then #eia608_start should be called again to resume
+ * the EIA-608 signal (if needed).
+ *
+ * @see #eia608_start
+ */
+LIBDRAGON_PREVIEW_API
+void eia608_stop(void);
+
+/**
+ * @brief Emit raw data to the EIA-608 signal.
+ * @preview
+ *
+ * This is an advanced function that allows you to emit raw data to the
+ * EIA-608 signal. It is mainly useful for testing or for emitting special
+ * commands that are currently not supported by the higher-level functions.
+ *
+ * Data is stored into an internal buffer that is then flushed as frames are
+ * displayed. Normally this buffer will never be full, but if it is, the
+ * function will return early and return the number of bytes emitted.
+ *
+ * @param data The data to emit (a 16-bit word)
+ * @param calc_parity If true, the parity bits will be calculated on the data.
+ * If false, the data will be sent as-is and thus probably
+ * be invalid if parity bits are not correct.
+ *
+ * @return True if the data was emitted, false if the buffer is full
+ */
+LIBDRAGON_PREVIEW_API
+bool eia608_write_raw(uint16_t data, bool calc_parity);
+
+/**
+ * @brief Write a control code to the EIA-608 signal.
+ * @preview
+ *
+ * @note Control codes are quite low level and their exact semantic is non-trivial,
+ * as it depends on the current state of the EIA-608 signal. Please
+ * check the EIA-608 standard for more information.
+ *
+ * @param ctrl The control code to write
+ */
+LIBDRAGON_PREVIEW_API
+void eia608_write_ctrl_raw(eia608_ctrl_t ctrl);
+
+/**
+ * @brief Emit a caption, with automatic positioning on the screen.
+ * @preview
+ *
+ * EIA-608 captions are divided into maximum 4 lines of 32 characters each.
+ * This function will automatically word-wrap the caption into multiple lines if
+ * necessary, but also respects any embedded newlines in the caption.
+ *
+ * The caption will not be displayed but just prepared. You need
+ * to call #eia608_caption_show to actually display it. This allows for perfect
+ * syncing as preparing a caption can take a non trivial amount of time
+ * (about 1 second every 60 characters), so to avoid avoid the captions always
+ * a bit late, you can prepare the next sentence in advance and then show it
+ * when needed.
+ *
+ * By default, the caption will be displayed centered, at the bottom of the
+ * screen, using white on black colors. You can change these parameters by
+ * setting fields in the \p parms structure.
+ *
+ * If the caption is too long to fit in the 4 lines, it will be truncated.
+ *
+ * The input string must be provided in UTF-8 format, but please notice
+ * that the EIA-608 standard only supports a limited set of characters,
+ * even though some extensions are available to cover at least western
+ * languages. Check the Wikipedia page for more information.
+ *
+ * @note This function emits the caption using the EIA-608 "POP-ON" mode,
+ * where the caption is accumulated into a back buffer and then displayed
+ * all at once. This is the most common mode for captions. If you call
+ * this function to display a caption before the previous one has been
+ * displayed, the new caption will replace the old one.
+ *
+ * @param cc Channel in which the caption must be prepared.
+ * @param utf8_str The UTF-8 string to display
+ * @param parms Optiona parameters to control the caption
+ * (use NULL for default values)
+ *
+ * @see Wikipedia article on EIA-608: https://en.wikipedia.org/wiki/EIA-608
+ */
+LIBDRAGON_PREVIEW_API
+void eia608_caption_prepare(eia608_channel_t cc, const char *utf8_str, eia608_captionparms_t *parms);
+
+/**
+ * @brief Show a caption that was previously prepared with #eia608_caption_prepare
+ * @preview
+ *
+ * This shows a caption that was prepared by #eia608_caption_prepare. The caption
+ * will be displayed on the screen for the specified duration, and then hidden.
+ *
+ * @param cc Channel in which the caption was prepared.
+ * @param duration_secs The duration in seconds to display the caption
+ */
+LIBDRAGON_PREVIEW_API
+void eia608_caption_show(eia608_channel_t cc, float duration_secs);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
\ No newline at end of file
diff --git a/include/emux.h b/include/emux.h
new file mode 100644
index 000000000..076391bcc
--- /dev/null
+++ b/include/emux.h
@@ -0,0 +1,458 @@
+/**
+ * @file emux.h
+ * @author Giovanni Bajo
+ * @brief Implementation of the N64 emulator extensions
+ * @ingroup emux
+ * @defgroup emux emux
+ *
+ * EMUX is an open spec for emulators to provide extensions to the emulated
+ * N64 system, allowing advanced profiling and debugging features.
+ * https://hackmd.io/SOLBoD6XSn-_WRdwtlMs9A
+ *
+ */
+
+#ifndef LIBDRAGON_EMUX_H
+#define LIBDRAGON_EMUX_H
+
+///@cond
+#ifndef __ASSEMBLER__
+#include
+#include "preview.h"
+#define cast64(x) (uint64_t)(x)
+#else
+#define cast64(x) x
+#endif
+///@endcond
+
+
+/** @brief Assemble a single emux opcode */
+#define EMUX_OP(ext, rd, rt, code) \
+ ((0b010000 << 26) | (1 << 25) | ((ext) & 0x3F) | \
+ (((rd) & 0x1F) << 20) | (((rt) & 0x1F) << 15) | (((code) & 0x1FF) << 6))
+
+/**
+ * @name EMUX opcodes
+ * @{
+ */
+#define EMUX_XDETECT(rd, code) EMUX_OP(0x20, rd, 0, code) ///< Detect EMUX presence
+#define EMUX_XBREAK() EMUX_OP(0x21, 0, 0, 0x000) ///< Trigger a breakpoint
+#define EMUX_XBREAKPOINT(addr, op) EMUX_OP(0x22, addr, 0, op) ///< Configure a breakpoint/watchpoint
+#define EMUX_XTRACESTART(count) EMUX_OP(0x23, 0, 0, count) ///< Start tracing
+#define EMUX_XTRACESTOP() EMUX_OP(0x24, 0, 0, 0x000) ///< Stop tracing
+#define EMUX_XLOG(addr, len, code) EMUX_OP(0x25, addr, len, code) ///< Log a string
+#define EMUX_XLOGREGS(mask, code) EMUX_OP(0x26, mask, 0, code) ///< Log registers
+#define EMUX_XHEXDUMP(addr, len) EMUX_OP(0x27, addr, len, 0x000) ///< Hexdump memory region
+#define EMUX_XPROF(slot, code) EMUX_OP(0x28, slot, 0, code) ///< Control profiler
+#define EMUX_XPROFREAD(slot, metric) EMUX_OP(0x29, slot, metric, 0x000) ///< Read profiler metric
+#define EMUX_XEXCEPTION(mask) EMUX_OP(0x2A, 0, mask, 0x000) ///< Set exception mask
+#define EMUX_XIOCTL(code) EMUX_OP(0x2C, 0, 0, code) ///< Modify emulator behavior
+/** @} */
+
+#define EMUX_FEAT1_DETECT (1 << 0x0) ///< EMUX detection support
+#define EMUX_FEAT1_BREAK (1 << 0x1) ///< Immediate breakpoint support
+#define EMUX_FEAT1_BREAKPOINTS (1 << 0x2) ///< Breakpoint configuration support
+#define EMUX_FEAT1_TRACE (1 << 0x3) ///< Tracing support
+#define EMUX_FEAT1_LOG (1 << 0x5) ///< Logging support
+#define EMUX_FEAT1_LOGREGS (1 << 0x6) ///< Register logging support
+#define EMUX_FEAT1_HEXDUMP (1 << 0x7) ///< Hexdump support
+#define EMUX_FEAT1_PROFILER (1 << 0x8) ///< Profiling support
+#define EMUX_FEAT1_EXCEPTION (1 << 0xA) ///< Exception support
+#define EMUX_FEAT1_IOCTL (1 << 0xC) ///< Emulator behavior support
+
+#define EMUX_LOG_ASCIIZ 0x000 ///< Log a zero-terminated string
+#define EMUX_LOG_LENGTH 0x001 ///< Log a non-zero-terminated string
+
+#define EMUX_BREAKPOINT_ADD 0x001 ///< Add instruction breakpoint
+#define EMUX_BREAKPOINT_REMOVE 0x002 ///< Remove instruction breakpoint
+#define EMUX_WATCHPOINT_READ_ADD 0x003 ///< Add read watchpoint
+#define EMUX_WATCHPOINT_READ_REMOVE 0x004 ///< Remove read watchpoint
+#define EMUX_WATCHPOINT_WRITE_ADD 0x005 ///< Add write watchpoint
+#define EMUX_WATCHPOINT_WRITE_REMOVE 0x006 ///< Remove write watchpoint
+
+#define EMUX_LOGREGS_COP0 0x000 ///< Dump COP0 registers
+#define EMUX_LOGREGS_COP1 0x001 ///< Dump COP1 registers
+#define EMUX_LOGREGS_COP2 0x002 ///< Dump COP2 registers
+#define EMUX_LOGREGS_GPR 0x003 ///< Dump GPR registers
+#define EMUX_LOGREGS_DECIMAL 0x004 ///< Prefer decimal formatting
+#define EMUX_LOGREGS_DOUBLE 0x008 ///< Interpret as 64-bit floating point (COP1)
+#define EMUX_LOGREGS_EXTRA 0x010 ///< Include extra registers
+
+#define EMUX_IOCTL_EXIT 0x001 ///< Exit the emulator
+#define EMUX_IOCTL_FAST 0x002 ///< Fast mode (go uncapped)
+#define EMUX_IOCTL_SLOW 0x003 ///< Slow mode (go 1x)
+#define EMUX_IOCTL_PAUSE 0x004 ///< Pause the emulator
+
+#define EMUX_PROF_START 0x001 ///< Start profiling on a slot
+#define EMUX_PROF_STOP 0x002 ///< Stop profiling on a slot
+#define EMUX_PROF_CLEAR 0x003 ///< Clear profiling data on a slot
+#define EMUX_PROF_RESET 0x004 ///< Reset profiling data (clear all slots)
+
+#define EMUX_PROF_CYCLES 0x0000 ///< Total CPU cycles
+#define EMUX_PROF_CYCLES_EXC 0x0001 ///< CPU cycles within exception
+#define EMUX_PROF_INSNS 0x0002 ///< CPU instructions executed
+#define EMUX_PROF_INSNS_EXC 0x0003 ///< CPU instructions executed within exception
+#define EMUX_PROF_ICACHE_HITS 0x0010 ///< Instruction cache hits
+#define EMUX_PROF_ICACHE_MISSES 0x0011 ///< Instruction cache misses
+#define EMUX_PROF_ICACHE_WBS 0x0012 ///< Instruction cache writebacks
+#define EMUX_PROF_DCACHE_HITS 0x0020 ///< Data cache hits
+#define EMUX_PROF_DCACHE_MISSES 0x0021 ///< Data cache misses
+#define EMUX_PROF_DCACHE_WBS 0x0022 ///< Data cache writebacks
+#define EMUX_PROF_TLB_HITS 0x0100 ///< TLB hits
+#define EMUX_PROF_TLB_MISSES 0x0101 ///< TLB misses
+#define EMUX_PROF_MTLB_HITS 0x0110 ///< MiniTLB hits
+#define EMUX_PROF_MTLB_MISSES 0x0111 ///< MiniTLB misses
+#define EMUX_PROF_RSP_CYCLES 0x0200 ///< RSP cycles
+#define EMUX_PROF_RSP_IDLE 0x0201 ///< RSP cycles in idle mode
+#define EMUX_PROF_RSP_STALLS 0x0210 ///< RSP stalls
+#define EMUX_PROF_RSP_STALLS_V 0x0211 ///< RSP stalls due to vector unit
+#define EMUX_PROF_RSP_STALLS_S 0x0212 ///< RSP stalls due to scalar unit
+#define EMUX_PROF_RAM_BYTES 0x0300 ///< RDRAM bytes transferred (r/w)
+#define EMUX_PROF_RAM_BYTES_R 0x0301 ///< RDRAM bytes transferred (read)
+#define EMUX_PROF_RAM_BYTES_W 0x0302 ///< RDRAM bytes transferred (write)
+#define EMUX_PROF_RAM_ICACHE_BYTES 0x0310 ///< RDRAM bytes transferred for I-cache (r/w)
+#define EMUX_PROF_RAM_ICACHE_BYTES_R 0x0311 ///< RDRAM bytes transferred for I-cache (read)
+#define EMUX_PROF_RAM_ICACHE_BYTES_W 0x0312 ///< RDRAM bytes transferred for I-cache (write)
+#define EMUX_PROF_RAM_DCACHE_BYTES 0x0320 ///< RDRAM bytes transferred for D-cache (r/w)
+#define EMUX_PROF_RAM_DCACHE_BYTES_R 0x0321 ///< RDRAM bytes transferred for D-cache (read)
+#define EMUX_PROF_RAM_DCACHE_BYTES_W 0x0322 ///< RDRAM bytes transferred for D-cache (write)
+#define EMUX_PROF_RAM_UNCACHED_BYTES 0x0330 ///< RDRAM bytes transferred for uncached accesses (r/w)
+#define EMUX_PROF_RAM_UNCACHED_BYTES_R 0x0331 ///< RDRAM bytes transferred for uncached accesses (read)
+#define EMUX_PROF_RAM_UNCACHED_BYTES_W 0x0332 ///< RDRAM bytes transferred for uncached accesses (write)
+#define EMUX_PROF_RAM_RSPDMA_BYTES 0x0340 ///< RDRAM bytes transferred via RSP DMA (r/w)
+#define EMUX_PROF_RAM_RSPDMA_BYTES_R 0x0341 ///< RDRAM bytes transferred via RSP DMA (read)
+#define EMUX_PROF_RAM_RSPDMA_BYTES_W 0x0342 ///< RDRAM bytes transferred via RSP DMA (write)
+#define EMUX_PROF_RAM_PIDMA_BYTES 0x0350 ///< RDRAM bytes transferred via PI DMA (r/w)
+#define EMUX_PROF_RAM_PIDMA_BYTES_R 0x0351 ///< RDRAM bytes transferred via PI DMA (read)
+#define EMUX_PROF_RAM_PIDMA_BYTES_W 0x0352 ///< RDRAM bytes transferred via PI DMA (write)
+#define EMUX_PROF_RAM_SIDMA_BYTES 0x0360 ///< RDRAM bytes transferred via SI DMA (r/w)
+#define EMUX_PROF_RAM_SIDMA_BYTES_R 0x0361 ///< RDRAM bytes transferred via SI DMA (read)
+#define EMUX_PROF_RAM_SIDMA_BYTES_W 0x0362 ///< RDRAM bytes transferred via SI DMA (write)
+#define EMUX_PROF_RAM_RDPDRAW_BYTES 0x0370 ///< RDRAM bytes transferred via RDP during draw (r/w)
+#define EMUX_PROF_RAM_RDPDRAW_BYTES_R 0x0371 ///< RDRAM bytes transferred via RDP during draw (read)
+#define EMUX_PROF_RAM_RDPDRAW_BYTES_W 0x0372 ///< RDRAM bytes transferred via RDP during draw (write)
+#define EMUX_PROF_RAM_AIDMA_BYTES 0x0380 ///< RDRAM bytes transferred via AI DMA (r/w)
+#define EMUX_PROF_RAM_AIDMA_BYTES_R 0x0381 ///< RDRAM bytes transferred via AI DMA (read)
+#define EMUX_PROF_RAM_AIDMA_BYTES_W 0x0382 ///< RDRAM bytes transferred via AI DMA (write) (always 0)
+#define EMUX_PROF_RAM_VI_BYTES 0x0390 ///< RDRAM bytes transferred via VI (r/w)
+#define EMUX_PROF_RAM_VI_BYTES_R 0x0391 ///< RDRAM bytes transferred via VI (read)
+#define EMUX_PROF_RAM_VI_BYTES_W 0x0392 ///< RDRAM bytes transferred via VI (write) (always 0)
+#define EMUX_PROF_RAM_RDPDMA_BYTES 0x03A0 ///< RDRAM bytes transferred via RDP DMA (r/w)
+#define EMUX_PROF_RAM_RDPDMA_BYTES_R 0x03A1 ///< RDRAM bytes transferred via RDP DMA (read)
+#define EMUX_PROF_RAM_RDPDMA_BYTES_W 0x03A2 ///< RDRAM bytes transferred via RDP DMA (write) (always 0)
+
+#define EMUX_EXCEPTION_ERR_MASK 0x00000000FFFFFFFFULL ///< Mask of exceptions related to hardware errors
+#define EMUX_EXCEPTION_WARN_MASK 0xFFFFFFFF00000000ULL ///< Mask of exceptions related to software warnings (resumable exceptions)
+
+#define EMUX_EXCEPTION_CPU_CACHED_ACCESS (cast64(1) << 1) ///< CPU cached access to non-RDRAM area
+#define EMUX_EXCEPTION_CPU_64BIT_READ (cast64(1) << 2) ///< CPU 64-bit read from non-RDRAM area
+#define EMUX_EXCEPTION_CPU_UNMAPPED_ACCESS (cast64(1) << 3) ///< CPU access to RCP unmapped area
+
+#ifndef __ASSEMBLER__
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Detect if EMUX is present and which features are supported
+ * @preview
+ *
+ * If this function returns zero, EMUX is not present. Otherwise, the
+ * returned bitmask contains the supported features (see EMUX_FEAT_* defines).
+ *
+ * @param subcode Index of the detection bitmask to read
+ * @return Bitmask of supported EMUX features (see EMUX_FEAT_*)
+ */
+LIBDRAGON_PREVIEW_API
+inline uint32_t emux_detect(int subcode)
+{
+ /*
+ * GCC can't easily feed the destination register number into a raw `.word`
+ * encoding. A simple MIPS64-friendly pattern is:
+ * 1) encode the instruction to write into a fixed scratch GPR (here: t0)
+ * 2) copy the scratch GPR into a GCC output register
+ */
+ const int REG_T0 = 8;
+ register uint64_t result asm("t0") = 0;
+
+ __asm__ __volatile__(
+ " .word %1\n"
+ : "+r"(result) : "i"(EMUX_XDETECT(REG_T0, subcode)) : "memory");
+
+ return result;
+}
+
+/**
+ * @brief Trigger an immediate breakpoint
+ * @preview
+ *
+ * When this function is called, if an EMUX-compatible emulator is running,
+ * execution will break immediately, allowing to inspect the current state
+ * of the emulated system.
+ */
+LIBDRAGON_PREVIEW_API
+inline void emux_break(void)
+{
+ __asm__ __volatile__(" .word %0\n" :: "i"(EMUX_XBREAK()): "memory");
+}
+
+/**
+ * @brief Log an UTF-8 string to the emulator
+ * @preview
+ *
+ * This function sends the provided UTF-8 string to the emulator logging
+ * system. The string must be 0-terminated.
+ *
+ * Notice that the emulator is allowed to perform line-buffering, so the
+ * logged string may not appear immediately in the logging output if it
+ * does not end with a newline character.
+ *
+ * @param ut8_str UTF-8 encoded string to log
+ */
+LIBDRAGON_PREVIEW_API
+inline void emux_log(const char *ut8_str)
+{
+ const int REG_T0 = 8;
+ register const char *__ut8_str asm("t0") = ut8_str;
+ __asm__ __volatile__(
+ " .word %1\n"
+ :: "r"(__ut8_str), "i"(EMUX_XLOG(REG_T0, 0, EMUX_LOG_ASCIIZ)) : "memory");
+}
+
+/**
+ * @brief Log a non-zero-terminated UTF-8 string to the emulator
+ * @preview
+ *
+ * This function sends the provided UTF-8 string to the emulator logging
+ * system. The string is not required to be 0-terminated; its length
+ * is provided as a separate parameter.
+ *
+ * Notice that the emulator is allowed to perform line-buffering, so the
+ * logged string may not appear immediately in the logging output if it
+ * does not end with a newline character.
+ *
+ * @param utf8_str UTF-8 encoded string to log
+ * @param len Length of the string
+ */
+LIBDRAGON_PREVIEW_API
+inline void emux_logn(const char *utf8_str, int len)
+{
+ const int REG_T0 = 8;
+ const int REG_T1 = 9;
+ register const char *__utf8_str asm("t0") = utf8_str;
+ register int __len asm("t1") = len;
+ __asm__ __volatile__(
+ " .word %2\n"
+ :: "r"(__utf8_str), "r"(__len), "i"(EMUX_XLOG(REG_T0, REG_T1, EMUX_LOG_LENGTH)) : "memory");
+}
+
+/**
+ * @brief Do a hexdump log to the emulator
+ * @preview
+ *
+ * The emulator will log the provided string as a hexdump, showing the
+ * hexadecimal values of each byte in the string (and possibly also its
+ * ASCII representation).
+ *
+ * @param buffer UTF-8 encoded string to log
+ * @param len Length of the string
+ */
+LIBDRAGON_PREVIEW_API
+inline void emux_hexdump(const uint8_t *buffer, int len)
+{
+ const int REG_T0 = 8;
+ const int REG_T1 = 9;
+ register const uint8_t *__buffer asm("t0") = buffer;
+ register int __len asm("t1") = len;
+ __asm__ __volatile__(
+ " .word %2\n"
+ :: "r"(__buffer), "r"(__len), "i"(EMUX_XHEXDUMP(REG_T0, REG_T1)) : "memory");
+}
+
+/**
+ * @brief Ask the emulator to exit
+ * @preview
+ *
+ * When this function is called, if an EMUX-compatible emulator is running,
+ * it will exit the emulation immediately.
+ *
+ * This is useful mainly for automated test ROMs, to save the user the hassle of
+ * closing the emulator window manually after the ROM has finished running.
+ */
+LIBDRAGON_PREVIEW_API
+inline void emux_ioctl_exit(void)
+{
+ __asm__ __volatile__(" .word %0\n" :: "i"(EMUX_XIOCTL(EMUX_IOCTL_EXIT)) : "memory");
+}
+
+/**
+ * @brief Set the emulator to fast mode
+ * @preview
+ *
+ * When this function is called, if an EMUX-compatible emulator is running,
+ * it will switch to fast mode, where the emulation runs as fast as possible
+ * (unlimited framerate).
+ *
+ * This is useful to quickly skip to the point that you want to test/debug.
+ */
+LIBDRAGON_PREVIEW_API
+inline void emux_ioctl_fast(void)
+{
+ __asm__ __volatile__(" .word %0\n" :: "i"(EMUX_XIOCTL(EMUX_IOCTL_FAST)) : "memory");
+}
+
+/**
+ * @brief Set the emulator to slow mode
+ * @preview
+ *
+ * When this function is called, if an EMUX-compatible emulator is running,
+ * it will switch to slow mode, where the emulation runs at real N64 speed
+ * (capped framerate).
+ */
+LIBDRAGON_PREVIEW_API
+inline void emux_ioctl_slow(void)
+{
+ __asm__ __volatile__(" .word %0\n" :: "i"(EMUX_XIOCTL(EMUX_IOCTL_SLOW)) : "memory");
+}
+
+/**
+ * @brief Pause the emulator
+ * @preview
+ *
+ * When this function is called, if an EMUX-compatible emulator is running,
+ * it will pause the emulation until resumed by the user.
+ *
+ * This can be useful to show the user a specific state/frame of the
+ * emulation for inspection.
+ */
+LIBDRAGON_PREVIEW_API
+inline void emux_ioctl_pause(void)
+{
+ __asm__ __volatile__(" .word %0\n" :: "i"(EMUX_XIOCTL(EMUX_IOCTL_PAUSE)) : "memory");
+}
+
+/**
+ * @brief Start profiling on a slot
+ * @preview
+ *
+ * When this function is called, if an EMUX-compatible emulator is running,
+ * it will start profiling on the specified slot.
+ *
+ * @param slot Profiler slot to start
+ */
+LIBDRAGON_PREVIEW_API
+inline void emux_prof_start(int slot)
+{
+ const int REG_T0 = 8;
+ register int __slot asm("t0") = slot;
+ __asm__ __volatile__(" .word %1\n" ::
+ "r"(__slot), "i"(EMUX_XPROF(REG_T0, EMUX_PROF_START)) : "memory");
+}
+
+/**
+ * @brief Stop profiling on a slot
+ * @preview
+ *
+ * When this function is called, if an EMUX-compatible emulator is running,
+ * it will stop profiling on the specified slot.
+ *
+ * @param slot Profiler slot to stop
+ */
+LIBDRAGON_PREVIEW_API
+inline void emux_prof_stop(int slot)
+{
+ const int REG_T0 = 8;
+ register int __slot asm("t0") = slot;
+ __asm__ __volatile__(" .word %1\n" ::
+ "r"(__slot), "i"(EMUX_XPROF(REG_T0, EMUX_PROF_STOP)) : "memory");
+}
+
+/**
+ * @brief Clear profiling data on a slot
+ * @preview
+ *
+ * When this function is called, if an EMUX-compatible emulator is running,
+ * it will clear profiling data on the specified slot.
+ *
+ * @param slot Profiler slot to clear
+ */
+LIBDRAGON_PREVIEW_API
+inline void emux_prof_clear(int slot)
+{
+ const int REG_T0 = 8;
+ register int __slot asm("t0") = slot;
+ __asm__ __volatile__(" .word %1\n" ::
+ "r"(__slot), "i"(EMUX_XPROF(REG_T0, EMUX_PROF_CLEAR)) : "memory");
+}
+
+/**
+ * @brief Reset all profiling data
+ * @preview
+ *
+ * When this function is called, if an EMUX-compatible emulator is running,
+ * it will reset all profiling data on all slots.
+ */
+LIBDRAGON_PREVIEW_API
+inline void emux_prof_reset(void)
+{
+ __asm__ __volatile__(" .word %0\n" :: "i"(EMUX_XPROF(0, EMUX_PROF_RESET)) : "memory");
+}
+
+/**
+ * @brief Read a profiling metric from a slot
+ * @preview
+ *
+ * When this function is called, if an EMUX-compatible emulator is running,
+ * it will read the specified profiling metric from the specified slot.
+ *
+ * This is useful to extract profiling data programmatically from within
+ * the emulated program. Emulators can also expose profiling data via their
+ * own user interface.
+ *
+ * @param slot Profiler slot to read from
+ * @param metric Metric to read (see EMUX_PROF_* defines)
+ * @return Value of the requested metric in the slot
+ */
+LIBDRAGON_PREVIEW_API
+inline uint64_t emux_prof_read(int slot, uint32_t metric)
+{
+ const int REG_T0 = 8;
+ const int REG_T1 = 9;
+ register int __slot asm("t0") = slot;
+ register uint64_t __metric asm("t1") = metric;
+
+ __asm__ __volatile__(
+ " .word %2\n"
+ : "+r"(__metric) : "r"(__slot), "i"(EMUX_XPROFREAD(REG_T0, REG_T1)) : "memory"
+ );
+
+ return __metric;
+}
+
+/**
+ * @brief Activate emux exceptions
+ * @preview
+ *
+ * This function allows to configure the emux exception mask, that tells the
+ * emulator which special exceptions should be generated.
+ *
+ * Emux exceptions are generated by the emulator as an alternative of properly
+ * emulating hardware freezes, to provide a better development experience.
+ *
+ * @param mask Mask of exceptions to activate/deactivate (see EMUX_EXCEPTION_* defines)
+ */
+LIBDRAGON_PREVIEW_API
+inline void emux_exception_set_mask(uint64_t mask)
+{
+ const int REG_T0 = 8;
+ register uint64_t __mask asm("t0") = mask;
+ __asm__ __volatile__(" .word %1\n" :: "r"(__mask), "i"(EMUX_XEXCEPTION(REG_T0)) : "memory");
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // __ASSEMBLER__
+
+#endif // LIBDRAGON_EMUX_H
diff --git a/include/emux.inc b/include/emux.inc
new file mode 100644
index 000000000..480a95906
--- /dev/null
+++ b/include/emux.inc
@@ -0,0 +1,136 @@
+############################################################
+# emux.inc - Implementation of the EMUX extensions
+#
+# Authors: Giovanni Bajo
+############################################################
+#ifndef LIBDRAGON_EMUX_INC
+#define LIBDRAGON_EMUX_INC
+
+#include "emux.h"
+
+.macro xdetect rd, code
+ .word EMUX_XDETECT(\rd, \code)
+.endm
+
+.macro xbreak
+ .word EMUX_XBREAK()
+.endm
+
+.macro xbreakpoint rd, op
+ .word EMUX_XBREAKPOINT(\rd, \op)
+.endm
+
+.macro xtracestart num_insn=0
+ .word EMUX_XTRACESTART(\num_insn)
+.endm
+
+.macro xtracestop
+ .word EMUX_XTRACESTOP()
+.endm
+
+.macro xlog_string str
+ .data
+__xlog_string_\@: .asciiz "\str"
+ .previous
+ .set noat
+ la $1, __xlog_string_\@
+ .word EMUX_XLOG(1, 0, EMUX_LOG_ASCIIZ)
+ .set at
+.endm
+
+.macro __xlogregs_gpr_rec reg0, regs:vararg
+ hexGeneralRegisters
+ .set gpr_mask, gpr_mask + (1 << hex.\reg0)
+ .ifnb \regs
+ __xlogregs_gpr_rec \regs
+ .endif
+.endm
+
+.macro xlogregs_gpr regs:vararg
+ .ifb \regs
+ .set gpr_mask, 0
+ .else
+ .set gpr_mask, 0
+ __xlogregs_gpr_rec \regs
+ .endif
+ .set noat
+ li $1, gpr_mask
+ .word EMUX_XLOGREGS(1, EMUX_LOGREGS_GPR)
+ .set at
+.endm
+
+.macro __xlogregs_vpr_rec reg0, regs:vararg
+ hexRegisters
+ .set vpr_mask, vpr_mask + (1 << \reg0)
+ .ifnb \regs
+ __xlogregs_vpr_rec \regs
+ .endif
+.endm
+
+.macro xlogregs_vpr regs:vararg
+ .ifb \regs
+ .set vpr_mask, 0
+ .else
+ .set vpr_mask, 0
+ __xlogregs_vpr_rec \regs
+ .endif
+ .set noat
+ li $1, vpr_mask
+ .word EMUX_XLOGREGS(1, EMUX_LOGREGS_COP2)
+ .set at
+.endm
+
+.macro xlogregs_vpr_accum regs:vararg
+ .ifb \regs
+ .set vpr_mask, 0
+ .else
+ .set vpr_mask, 0
+ __xlogregs_vpr_rec \regs
+ .endif
+ .set noat
+ li $1, vpr_mask
+ .word EMUX_XLOGREGS(1, EMUX_LOGREGS_COP2 | EMUX_LOGREGS_EXTRA)
+ .set at
+.endm
+
+.macro __xlogregs_cop0_rec reg0, regs:vararg
+ .set cop0reg.$0, 0
+ .set cop0reg.$1, 1
+ .set cop0reg.$2, 2
+ .set cop0reg.$3, 3
+ .set cop0reg.$4, 4
+ .set cop0reg.$5, 5
+ .set cop0reg.$6, 6
+ .set cop0reg.$7, 7
+ .set cop0reg.$8, 8
+ .set cop0reg.$9, 9
+ .set cop0reg.$10, 10
+ .set cop0reg.$11, 11
+ .set cop0reg.$12, 12
+ .set cop0reg.$13, 13
+ .set cop0reg.$14, 14
+ .set cop0reg.$15, 15
+ .set cop0_mask, cop0_mask + (1 << cop0reg.\reg0)
+ .ifnb \regs
+ __xlogregs_cop0_rec \regs
+ .endif
+.endm
+
+.macro xlogregs_cop0 regs:vararg
+ .ifb \regs
+ .set cop0_mask, 0
+ .else
+ .set cop0_mask, 0
+ __xlogregs_cop0_rec \regs
+ .endif
+ .set noat
+ li $1, cop0_mask
+ .word EMUX_XLOGREGS(1, EMUX_LOGREGS_COP0)
+ .set at
+.endm
+
+.macro xioctl_exit
+ .word EMUX_XIOCTL(EMUX_IOCTL_EXIT)
+.endm
+
+#endif // LIBDRAGON_EMUX_INC
diff --git a/include/entropy.h b/include/entropy.h
new file mode 100644
index 000000000..a4e77e1aa
--- /dev/null
+++ b/include/entropy.h
@@ -0,0 +1,101 @@
+/**
+ * @file entropy.h
+ * @author Giovanni Bajo
+ * @brief Header file for entropy source functions.
+ *
+ * This module implements an entropy source for non-deterministic random
+ * number generation. It is implemented via two functions:
+ *
+ * - `getentropy` (BSD style): fills a buffer with non-deterministic random data.
+ * - `getentropy32`: returns a 32-bit non-deterministic random number.
+ *
+ * Notice that this entropy source is not a cryptographic one. If you need a
+ * cryptographically secure random number generator, you should fetch some
+ * entropy from this module and then use a cryptographic PRNG like ChaCha20
+ * or SipHash to generate the random numbers.
+ *
+ * @note This is a low-level module for advance use cases. If you just
+ * need some random numbers for your game, use this module just to seed the
+ * standard C library `rand()` function and then use `rand()` in your game
+ * engine:
+ *
+ * \code{c}
+ * // Seed the C library random number generator in an unpredictable way
+ * // using the entropy source, so that the random numbers generated will
+ * // be different on each boot.
+ * srand(getentropy32());
+ *
+ * // Now you can use rand() to generate random numbers in your game.
+ * int random_stage = rand() % NUM_STAGES;
+ * \endcode
+ */
+#ifndef LIBDRAGON_ENTROPY_H
+#define LIBDRAGON_ENTROPY_H
+
+#include
+#include
+#include "preview.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Generate an array of unpredictable random numbers
+ * @preview
+ *
+ * This function can be used to generate an array of random data. The function
+ * is guaranteed to return good quality random numbers of basically
+ * unlimited length. The function is automatically seeded by entropy collected
+ * during the boot process (during IPL3) so it always returns different
+ * numbers after each boot on hardware. On each emulator, though, the
+ * generate numbers will be consistent.
+ *
+ * The data returned by this function is not cryptographically secure, but
+ * there aren't currently many known use cases in which this is a problem.
+ * Instead, the numbers are non reproducible and unpredictable, meaning that
+ * will vary on different runs.
+ *
+ * @param buf Output buffer
+ * @param len Length of the output buffer
+ * @return 0 on success, -1 on failure. Currently, the function
+ * never returns -1.
+ */
+LIBDRAGON_PREVIEW_API
+int getentropy(void *buf, size_t len);
+
+/**
+ * @brief Return 64-bit of entropy.
+ * @preview
+ *
+ * This is a simplified API for getentropy() to just return 64-bit of entropy
+ * instead of an arbitrary buffer.
+ *
+ * @note This function is much, much slower than calling rand(). If you just
+ * need random numbers for your game, use rand() instead.
+ *
+ * @return Unpredictable 64-bit random number
+ */
+ LIBDRAGON_PREVIEW_API
+ uint64_t getentropy64(void);
+
+ /**
+ * @brief Return 32-bit of entropy.
+ * @preview
+ *
+ * This is a simplified API for getentropy() to just return 32-bit of entropy
+ * instead of an arbitrary buffer. Useful for instance to seed `srand()`.
+ *
+ * @note This function is much, much slower than calling rand(). If you just
+ * need random numbers for your game, use rand() instead.
+ *
+ * @return Unpredictable 32-bit random number
+ */
+LIBDRAGON_PREVIEW_API
+uint32_t getentropy32(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/exception.h b/include/exception.h
index 80ddfdd00..287238f43 100644
--- a/include/exception.h
+++ b/include/exception.h
@@ -61,6 +61,7 @@ typedef enum {
EXCEPTION_CODE_TRAP = 13,
EXCEPTION_CODE_FLOATING_POINT = 15,
EXCEPTION_CODE_WATCH = 23,
+ EXCEPTION_CODE_EMUX = 24,
} exception_code_t;
/**
diff --git a/include/fat.h b/include/fat.h
new file mode 100644
index 000000000..964d60952
--- /dev/null
+++ b/include/fat.h
@@ -0,0 +1,266 @@
+/**
+ * @file fat.h
+ * @author Giovanni Bajo
+ * @brief FAT filesystem interface
+ * @ingroup lowlevel
+ *
+ * This file allows multiple clients to access and use the FatFs library
+ * within libdragon for different scopes.
+ *
+ * FatFs is a generic FAT filesystem module for small embedded systems,
+ * written by ChaN. It is available at http://elm-chan.org/fsw/ff/00index_e.html.
+ *
+ * FatFs is currently used by libdragon for a single use case: to implement
+ * access to the SD card in flashcarts. This access is currently implemented
+ * by the debug library (debug.h), initialized via #debug_init_sdfs.
+ *
+ * The APIs exported by this file are useful only if you need to mount a FAT
+ * volume coming from some other sources (eg: a FAT image within a ROM, or
+ * a FAT volume accessible via some custom USB protocol, or whatever else).
+ * If you need this, call #fat_mount to configure a FatFs volume, which you
+ * will then be able to access via standard C file operations.
+ */
+
+#ifndef LIBDRAGON_FAT_H
+#define LIBDRAGON_FAT_H
+
+#include
+#include "preview.h"
+#include "ioctl.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Interface for disk operations required to implement a volume.
+ *
+ * These interfaces are identical to diskio.h from FatFs. It basically just
+ * adds one indirection layer to it, to dispatch the calls to the correct
+ * volume.
+ */
+typedef struct {
+ /**
+ * @brief Initialize the underlying device.
+ *
+ * This function is called at mount time.
+ *
+ * @return Disk status codes (see fatfs/diskio.h)
+ * @see http://elm-chan.org/fsw/ff/doc/dinit.html
+ */
+ int (*disk_initialize)(void);
+
+ /**
+ * @brief Return the current status of the underlying device.
+ *
+ * Return the current status of the device. The status can be
+ * a combination of:
+ *
+ * * STA_NOINIT: device must be reinitialized
+ * * STA_NODISK: no medium in the device
+ * * STA_PROTECT: write protected
+ *
+ * @return Disk status codes (see fatfs/diskio.h)
+ * @see http://elm-chan.org/fsw/ff/doc/dstat.html
+ */
+ int (*disk_status)(void);
+
+ /**
+ * @brief Read sectors from the underlying device into RDRAM.
+ *
+ * This function reads sectors from the underlying device into RDRAM.
+ *
+ * @return Result code (see fatfs/diskio.h)
+ * @see http://elm-chan.org/fsw/ff/doc/dread.html
+ */
+ int (*disk_read)(uint8_t* buff, int64_t sector, int count);
+
+ /**
+ * @brief Write sectors from RDRAM to the underlying device.
+ *
+ * This function writes sectors from RDRAM to the underlying device.
+ *
+ * @return Result code (see fatfs/diskio.h)
+ * @see http://elm-chan.org/fsw/ff/doc/dwrite.html
+ */
+ int (*disk_write)(const uint8_t* buff, int64_t sector, int count);
+
+ /**
+ * @brief Perform a I/O request on the underlying device.
+ *
+ * This function performs a I/O request on the underlying device.
+ * FatFs uses this function to perform various operations like
+ * getting the sector count, the sector size, etc. It can be
+ * used to implement custom operations as well.
+ *
+ * @return Result code (see fatfs/diskio.h)
+ * @see http://elm-chan.org/fsw/ff/doc/dioctl.html
+ */
+ int (*disk_ioctl)(uint8_t cmd, void* buff);
+} fat_disk_t;
+
+
+/**
+ * @brief Return the current cluster number
+ *
+ * The cluster number is a 32-bit integer value.
+ *
+ * \code{.c}
+ * FILE *f = fopen("sd:/myfile.dat", "rb");
+ * int32_t cluster = 0;
+ * ioctl(fileno(f), IOFAT_GET_CLUSTER, &cluster);
+ * \endcode
+ */
+#define IOFAT_GET_CLUSTER _IO('F', 1)
+
+/**
+ * @brief Return the current sector number
+ *
+ * The sector number is a 64-bit integer value.
+ *
+ * \code{.c}
+ * FILE *f = fopen("sd:/myfile.dat", "rb");
+ * int64_t sector = 0;
+ * ioctl(fileno(f), IOFAT_GET_SECTOR, §or);
+ * \endcode
+ */
+#define IOFAT_GET_SECTOR _IO('F', 2)
+
+/**
+ * @brief Return the size of a cluster, as number of sectors (not bytes)
+ *
+ * The cluster size is a 32-bit integer value. It is a property of the filesystem
+ * so it is not file-dependent, but access to a file is still currently required
+ * to retrieve it.
+ *
+ * \code{.c}
+ * FILE *f = fopen("sd:/myfile.dat", "rb");
+ * int32_t cluster_size = 0;
+ * ioctl(fileno(f), IOFAT_GET_CLUSTER_SIZE, &cluster_size);
+ * \endcode
+ */
+#define IOFAT_GET_CLUSTER_SIZE _IO('F', 3)
+
+/**
+ * @brief FAT file attribute bits (matching FatFS AM_* constants)
+ *
+ * When stat() or fstat() is called on a file from a FAT-mounted volume
+ * (e.g. "sd:/path/file"), the st_rdev field contains the raw FAT attribute
+ * byte. Use these masks with #FAT_ATTR() to extract individual attributes.
+ */
+#define S_FAT_ATTR_RDO 0x01 /**< Read only */
+#define S_FAT_ATTR_HID 0x02 /**< Hidden */
+#define S_FAT_ATTR_SYS 0x04 /**< System */
+#define S_FAT_ATTR_DIR 0x10 /**< Directory */
+#define S_FAT_ATTR_ARC 0x20 /**< Archive */
+
+/**
+ * @brief Extract FAT attributes from a struct stat
+ * @param st Pointer to struct stat from stat() or fstat() on a FAT file
+ * @return Raw FAT attribute byte (0x01-0x3F); only meaningful for FAT-mounted files
+ */
+#define FAT_ATTR(st) ((unsigned int)((st)->st_rdev))
+
+/**
+ * @brief Test if file has FAT read-only attribute
+ * @param st Pointer to struct stat from stat() or fstat() on a FAT file
+ */
+#define FAT_ATTR_IS_RDO(st) (FAT_ATTR(st) & S_FAT_ATTR_RDO)
+
+/**
+ * @brief Test if file has FAT hidden attribute
+ * @param st Pointer to struct stat from stat() or fstat() on a FAT file
+ */
+#define FAT_ATTR_IS_HID(st) (FAT_ATTR(st) & S_FAT_ATTR_HID)
+
+/**
+ * @brief Test if file has FAT system attribute
+ * @param st Pointer to struct stat from stat() or fstat() on a FAT file
+ */
+#define FAT_ATTR_IS_SYS(st) (FAT_ATTR(st) & S_FAT_ATTR_SYS)
+
+/**
+ * @brief Test if file has FAT archive attribute
+ * @param st Pointer to struct stat from stat() or fstat() on a FAT file
+ */
+#define FAT_ATTR_IS_ARC(st) (FAT_ATTR(st) & S_FAT_ATTR_ARC)
+
+/**
+ * @brief Mount the volume only when it is accessed for the first time.
+ *
+ * This flag can be passed to #fat_mount to defer the actual mounting of the
+ * volume until it is accessed for the first time. This can be useful to
+ * avoid blocking the application for a long time during the mount operation.
+ *
+ * When you pass this flag, fat_mount will return immediately after configuring
+ * the internal data structure, but no I/O operation will be performed on the
+ * volume.
+ */
+#define FAT_MOUNT_DEFERRED 0x0001
+
+
+/**
+ * @brief Mount a new FAT volume through the FatFs library.
+ * @preview
+ *
+ * This function allows to mount a new FAT volume through the FatFs library.
+ * Access to the actual disk is done through the provided disk operations,
+ * so that the volume can be backed by any kind of storage.
+ *
+ * After calling this function, you will be able to access the files on the
+ * volume using two different APIs:
+ *
+ * * Standard C file operations (fopen, fread, fwrite, fclose, etc), or
+ * POSIX file operations (open, read, write, close, etc). This is the preferred
+ * way to access files, as it is the most portable and the most familiar to
+ * most developers. Files will be accessed using the prefix provided in the
+ * call to this function. For instance, if you provide "sd:" as the prefix,
+ * you will be able to access the files on the volume using paths like
+ * "sd:/path/to/file.txt".
+ * * Direct FatFs API calls. This is the low-level API provided by the FatFs
+ * library itself. It is less portable and less familiar to most developers,
+ * but it might be required for certain very low level operations, like
+ * inspecting the actual FAT chains of a file. The volume ID for direct
+ * FatFs API usage will be returned by this function. To use this API, you
+ * will need to include the FatFs headers in your source files ("fatfs/fs.h"),
+ * and then refer to filename paths using the volume ID. For instance, if
+ * the volume ID is 2, you will be able to access the files on the volume
+ * using paths like "2:/path/to/file.txt".
+ *
+ * @param prefix Prefix to use for the volume in stdio calls like
+ * fopen (eg: "sd:"). If this is NULL, the volume
+ * will not be accessible via standard C API, but only
+ * via the FatFs API.
+ * @param disk Table of disk operations to use for this volume
+ * @param flags Flags to affect the behavior of the mount operation.
+ * You can pass 0 as default, or one of the various
+ * FAT_MOUNT_ flags.
+ *
+ * @return >= 0 on success: the value will be the volume ID for direct FatFs API
+ * usage
+ * @return -1 on mount failure (errno will be set). Eg: corrupted FAT header
+ */
+LIBDRAGON_PREVIEW_API
+int fat_mount(const char *prefix, const fat_disk_t* disk, int flags);
+
+/**
+ * @brief Unmount a FAT volume previously mounted with #fat_mount.
+ * @preview
+ *
+ * This function releases a FAT volume identified by the volume ID returned by
+ * #fat_mount. If the volume was mounted with a stdio prefix, the filesystem
+ * prefix is detached as well.
+ *
+ * @param vol_id Volume ID returned by #fat_mount.
+ *
+ * @return 0 on success
+ * @return -1 on failure (errno will be set)
+ */
+LIBDRAGON_PREVIEW_API
+int fat_unmount(int vol_id);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/fgeom.h b/include/fgeom.h
new file mode 100644
index 000000000..b2b2fb102
--- /dev/null
+++ b/include/fgeom.h
@@ -0,0 +1,759 @@
+/**
+ * @file fgeom.h
+ * @author Dennis Heinze
+ * @brief Structs and functions for common 3D geometry calculations.
+ * @ingroup fastmath
+ */
+
+#ifndef LIBDRAGON_FGEOM_H
+#define LIBDRAGON_FGEOM_H
+
+#include
+#include
+#include
+#include "preview.h"
+#include "fmath.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Three dimensional vector with single precision floating point components.
+ *
+ * The components can either be accessed via named fields (x, y, z),
+ * or by index using the v field.
+ */
+typedef union {
+ struct {
+ float x, y, z; ///< Named fields to access individual vector components directly.
+ };
+ float v[3]; ///< Array field to access the vector components via index.
+} fm_vec3_t;
+
+/**
+ * @brief Four dimensional vector with single precision floating point components.
+ *
+ * The components can either be accessed via named fields (x, y, z, w),
+ * or by index using the v field.
+ */
+typedef union {
+ struct {
+ float x, y, z, w; ///< Named fields to access individual vector components directly.
+ };
+ float v[4]; ///< Array field to access the vector components via index.
+} fm_vec4_t;
+
+/**
+ * @brief A quaternion with single precision floating point components.
+ *
+ * The components can either be accessed via named fields (x, y, z, w),
+ * or by index using the v field.
+ */
+typedef union {
+ struct {
+ float x, y, z, w; ///< Named fields to access individual quaternion components directly.
+ };
+ float v[4]; ///< Array field to access the quaternion components via index.
+} fm_quat_t;
+
+/**
+ * @brief 4x4 Matrix with single precision floating point components.
+ */
+typedef struct {
+ float m[4][4]; ///< Two-dimensional array that contains the matrix coefficients in column-major order.
+} fm_mat4_t;
+
+/******** VEC3 **********/
+
+/**
+ * @brief Negate a 3D vector.
+ * @preview
+ * @note This function still works if input and output are in the same memory location.
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_vec3_negate(fm_vec3_t *out, const fm_vec3_t *v)
+{
+ for (int i = 0; i < 3; i++) out->v[i] = -v->v[i];
+}
+
+/**
+ * @brief Add two 3D vectors component-wise.
+ * @preview
+ * @note This function still works if input and output are in the same memory location.
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_vec3_add(fm_vec3_t *out, const fm_vec3_t *a, const fm_vec3_t *b)
+{
+ for (int i = 0; i < 3; i++) out->v[i] = a->v[i] + b->v[i];
+}
+
+/**
+ * @brief Subtract two 3D vectors component-wise.
+ * @preview
+ * @note This function still works if input and output are in the same memory location.
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_vec3_sub(fm_vec3_t *out, const fm_vec3_t *a, const fm_vec3_t *b)
+{
+ for (int i = 0; i < 3; i++) out->v[i] = a->v[i] - b->v[i];
+}
+
+/**
+ * @brief Multiply two 3D vectors component-wise.
+ * @preview
+ * @note If you need the dot product, use #fm_vec3_dot instead.
+ * If you need the cross product, use #fm_vec3_cross instead.
+ * @note This function still works if input and output are in the same memory location.
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_vec3_mul(fm_vec3_t *out, const fm_vec3_t *a, const fm_vec3_t *b)
+{
+ for (int i = 0; i < 3; i++) out->v[i] = a->v[i] * b->v[i];
+}
+
+/**
+ * @brief Divide two 3D vectors component-wise.
+ * @preview
+ * @note This function still works if input and output are in the same memory location.
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_vec3_div(fm_vec3_t *out, const fm_vec3_t *a, const fm_vec3_t *b)
+{
+ for (int i = 0; i < 3; i++) out->v[i] = a->v[i] / b->v[i];
+}
+
+/**
+ * @brief Scale a 3D vector by a factor.
+ * @preview
+ * @note This function still works if input and output are in the same memory location.
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_vec3_scale(fm_vec3_t *out, const fm_vec3_t *a, float s)
+{
+ for (int i = 0; i < 3; i++) out->v[i] = a->v[i] * s;
+}
+
+/**
+ * @brief Compute the dot product of two 3D vectors.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline float fm_vec3_dot(const fm_vec3_t *a, const fm_vec3_t *b)
+{
+ return a->x * b->x + a->y * b->y + a->z * b->z;
+}
+
+/**
+ * @brief Compute the square magnitude of a 3D vector.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline float fm_vec3_len2(const fm_vec3_t *a)
+{
+ return fm_vec3_dot(a, a);
+}
+
+/**
+ * @brief Compute the magnitude of a 3D vector.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline float fm_vec3_len(const fm_vec3_t *a)
+{
+ return sqrtf(fm_vec3_len2(a));
+}
+
+/**
+ * @brief Compute the square distance between two 3D vectors.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline float fm_vec3_distance2(const fm_vec3_t *a, const fm_vec3_t *b)
+{
+ fm_vec3_t diff;
+ fm_vec3_sub(&diff, a, b);
+ return fm_vec3_len2(&diff);
+}
+
+/**
+ * @brief Compute the distance between two 3D vectors.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline float fm_vec3_distance(const fm_vec3_t *a, const fm_vec3_t *b)
+{
+ return sqrtf(fm_vec3_distance2(a, b));
+}
+
+/**
+ * @brief Normalize a 3D vector.
+ * @preview
+ * @note This function still works if input and output are in the same memory location.
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_vec3_norm(fm_vec3_t *out, const fm_vec3_t *a)
+{
+ float len = fm_vec3_len(a);
+ if (len < FM_EPSILON) {
+ *out = (fm_vec3_t){};
+ return;
+ }
+ *out = (fm_vec3_t){{ a->x / len, a->y / len, a->z / len }};
+}
+
+/**
+ * @brief Compute the cross product of two 3D vectors.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_vec3_cross(fm_vec3_t *out, const fm_vec3_t *a, const fm_vec3_t *b)
+{
+ *out = (fm_vec3_t){{ a->y * b->z - a->z * b->y,
+ a->z * b->x - a->x * b->z,
+ a->x * b->y - a->y * b->x }};
+}
+
+/**
+ * @brief Linearly interpolate between two 3D vectors.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_vec3_lerp(fm_vec3_t *out, const fm_vec3_t *a, const fm_vec3_t *b, float t)
+{
+ for (int i = 0; i < 3; i++) out->v[i] = a->v[i] + (b->v[i] - a->v[i]) * t;
+}
+
+/**
+ * @brief Compute the reflection of an incident vector off a surface.
+ * @preview
+ *
+ * @param[out] out Will contain the reflected vector.
+ * @param[in] i The incident vector.
+ * @param[in] n The surface's normal vector. Must be normalized.
+ */
+LIBDRAGON_PREVIEW_API
+void fm_vec3_reflect(fm_vec3_t *out, const fm_vec3_t *i, const fm_vec3_t *n);
+
+/**
+ * @brief Compute the refraction of an incident vector through a surface.
+ * @preview
+ *
+ * @param[out] out Will contain the refracted vector.
+ * @param[in] i The incident vector. Must be normalized.
+ * @param[in] n The surface's normal vector. Must be normalized.
+ * @param[in] eta The ratio of indices of refraction (incident/transmitted)
+ * @return True if refraction occurs; false if total internal reflection occurs.
+ */
+LIBDRAGON_PREVIEW_API
+bool fm_vec3_refract(fm_vec3_t *out, const fm_vec3_t *i, const fm_vec3_t *n, float eta);
+
+/**
+ * @brief Rotates a given vector by a quaternion
+ * @preview
+ *
+ * @param out Will contain rotated vector
+ * @param i The vector to rotate
+ * @param q The quaternion
+ */
+LIBDRAGON_PREVIEW_API
+void fm_vec3_rotate(fm_vec3_t *out, const fm_vec3_t *i, const fm_quat_t *q);
+
+/******** QUAT **********/
+
+/**
+ * @brief Create an identity quaternion.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_quat_identity(fm_quat_t *out)
+{
+ *out = (fm_quat_t){{ 0, 0, 0, 1 }};
+}
+
+/**
+ * @brief Create a quaternion from euler angles, applying the rotations in ZYX order.
+ * @preview
+ *
+ * @param[out] out Will contain the created quaternion.
+ * @param[in] x Rotation around the X axis in radians.
+ * @param[in] y Rotation around the Y axis in radians.
+ * @param[in] z Rotation around the Z axis in radians.
+ */
+LIBDRAGON_PREVIEW_API
+void fm_quat_from_euler_zyx(fm_quat_t *out, float x, float y, float z);
+
+/**
+ * @brief Create a quaternion from an axis of rotation and an angle.
+ * @preview
+ * @param[out] out Will contain the created quaternion.
+ * @param[in] axis The axis of rotation. Must be normalized.
+ * @param[in] angle The angle in radians.
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_quat_from_axis_angle(fm_quat_t *out, const fm_vec3_t *axis, float angle)
+{
+ float s, c;
+ fm_sincosf(angle * 0.5f, &s, &c);
+
+ *out = (fm_quat_t){{ axis->x * s, axis->y * s, axis->z * s, c }};
+}
+
+/**
+ * @brief Multiply two quaternions.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+void fm_quat_mul(fm_quat_t *out, const fm_quat_t *a, const fm_quat_t *b);
+
+/**
+ * @brief Rotate a quaternion around an axis by some angle.
+ * @preview
+ * @note This is just a shorthand for #fm_quat_from_axis_angle and #fm_quat_mul.
+ */
+LIBDRAGON_PREVIEW_API
+void fm_quat_rotate(fm_quat_t *out, const fm_quat_t *q, const fm_vec3_t *axis, float angle);
+
+/**
+ * @brief Compute the dot product of two quaternions.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline float fm_quat_dot(const fm_quat_t *a, const fm_quat_t *b)
+{
+ return a->x * b->x + a->y * b->y + a->z * b->z + a->w * b->w;
+}
+
+/**
+ * @brief Compute the inverse of a quaternion.
+ * @preview
+ * @note The input quaternion must have non-zero magnitude.
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_quat_inverse(fm_quat_t *out, const fm_quat_t *q)
+{
+ float inv_mag2 = 1.0f / fm_quat_dot(q, q);
+ for (int i = 0; i < 3; i++) out->v[i] = -q->v[i] * inv_mag2;
+ out->w = q->w * inv_mag2;
+}
+
+/**
+ * @brief Normalize a quaternion.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_quat_norm(fm_quat_t *out, const fm_quat_t *q)
+{
+ float len = sqrtf(fm_quat_dot(q, q));
+ if (len < FM_EPSILON) {
+ fm_quat_identity(out);
+ return;
+ }
+ *out = (fm_quat_t){{ q->x / len, q->y / len, q->z / len, q->w / len }};
+}
+
+/**
+ * @brief Compute normalized linear interpolation between two quaternions.
+ * @preview
+ * @note This function is faster than #fm_quat_slerp, but produces a non-constant angular velocity when used for animation.
+ */
+LIBDRAGON_PREVIEW_API
+void fm_quat_nlerp(fm_quat_t *out, const fm_quat_t *a, const fm_quat_t *b, float t);
+
+
+/**
+ * @brief Compute spherical linear interpolation between two quaternions.
+ * @preview
+ * @note As opposed to #fm_quat_nlerp, this function produces constant angular velocity when used for animation, but is slower.
+ */
+LIBDRAGON_PREVIEW_API
+void fm_quat_slerp(fm_quat_t *out, const fm_quat_t *a, const fm_quat_t *b, float t);
+
+
+/******** MAT4 **********/
+
+/**
+ * @brief Create a 4x4 identity matrix.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_mat4_identity(fm_mat4_t *out)
+{
+ *out = (fm_mat4_t){};
+ out->m[0][0] = 1;
+ out->m[1][1] = 1;
+ out->m[2][2] = 1;
+ out->m[3][3] = 1;
+}
+
+/**
+ * @brief Apply scale to a 4x4 matrix.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_mat4_scale(fm_mat4_t *out, const fm_vec3_t *scale)
+{
+ for (int i=0; i<4; i++) out->m[i][0] *= scale->x;
+ for (int i=0; i<4; i++) out->m[i][1] *= scale->y;
+ for (int i=0; i<4; i++) out->m[i][2] *= scale->z;
+}
+
+/**
+ * @brief Apply translation to a 4x4 matrix.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_mat4_translate(fm_mat4_t *out, const fm_vec3_t *translate)
+{
+ for (int i=0; i<3; i++) out->m[3][i] += translate->v[i];
+}
+
+/**
+ * @brief Apply rotation to a 4x4 matrix.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+void fm_mat4_rotate(fm_mat4_t *out, const fm_quat_t *rotation);
+
+/**
+ * @brief Create a rotation matrix from an axis of rotation and an angle.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+void fm_mat4_from_axis_angle(fm_mat4_t *out, const fm_vec3_t *axis, float angle);
+
+/**
+ * @brief Create an affine transformation matrix from scale, rotation and translation.
+ * @preview
+ *
+ * The rotation is accepted as a quaternion.
+ */
+LIBDRAGON_PREVIEW_API
+void fm_mat4_from_srt(fm_mat4_t *out, const fm_vec3_t *scale, const fm_quat_t *quat, const fm_vec3_t *translate);
+
+/**
+ * @brief Create an affine transformation matrix from scale, rotation and translation.
+ * @preview
+ *
+ * The rotation is accepted as euler angles.
+ */
+LIBDRAGON_PREVIEW_API
+void fm_mat4_from_srt_euler(fm_mat4_t *out, const fm_vec3_t *scale, const float euler[3], const fm_vec3_t *translate);
+
+/**
+ * @brief Create a rigid transformation matrix from rotation and translation.
+ * @preview
+ *
+ * The rotation is accepted as a quaternion.
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_mat4_from_rt(fm_mat4_t *out, const fm_quat_t *quat, const fm_vec3_t *translate)
+{
+ const fm_vec3_t scale = {{1, 1, 1}};
+ fm_mat4_from_srt(out, &scale, quat, translate);
+}
+
+/**
+ * @brief Create a rigid transformation matrix from rotation and translation.
+ * @preview
+ *
+ * The rotation is accepted as euler angles.
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_mat4_from_rt_euler(fm_mat4_t *out, const float euler[3], const fm_vec3_t *translate)
+{
+ const fm_vec3_t scale = {{1, 1, 1}};
+ fm_mat4_from_srt_euler(out, &scale, euler, translate);
+}
+
+/**
+ * @brief Create a translation matrix.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_mat4_from_translation(fm_mat4_t *out, const fm_vec3_t *translate)
+{
+ fm_mat4_identity(out);
+ fm_mat4_translate(out, translate);
+}
+
+/**
+ * @brief Create a rotation matrix from a quaternion.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_mat4_from_rotation(fm_mat4_t *out, const fm_quat_t *rotation)
+{
+ const fm_vec3_t translate = {{0, 0, 0}};
+ fm_mat4_from_rt(out, rotation, &translate);
+}
+
+/**
+ * @brief Create a scale matrix.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_mat4_from_scale(fm_mat4_t *out, const fm_vec3_t *scale)
+{
+ fm_mat4_identity(out);
+ fm_mat4_scale(out, scale);
+}
+
+/**
+ * @brief Multiply two 4x4 matrices.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_mat4_mul(fm_mat4_t *out, const fm_mat4_t *a, const fm_mat4_t *b)
+{
+ fm_mat4_t tmp;
+ for (int i = 0; i < 4; i++)
+ {
+ for (int j = 0; j < 4; j++)
+ {
+ tmp.m[i][j] = a->m[0][j] * b->m[i][0] +
+ a->m[1][j] * b->m[i][1] +
+ a->m[2][j] * b->m[i][2] +
+ a->m[3][j] * b->m[i][3];
+ }
+ }
+ *out = tmp;
+}
+
+/**
+ * @brief Transpose a 4x4 matrix.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_mat4_transpose(fm_mat4_t *out, const fm_mat4_t *m)
+{
+ for (int i = 0; i < 4; i++)
+ {
+ for (int j = 0; j < 4; j++)
+ {
+ out->m[i][j] = m->m[j][i];
+ }
+ }
+}
+
+/**
+ * @brief Compute the determinant of a 4x4 matrix.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+float fm_mat4_det(const fm_mat4_t *m);
+
+/**
+ * @brief Compute the inverse of a 4x4 matrix.
+ * @preview
+ * @note The input matrix must be invertible.
+ */
+LIBDRAGON_PREVIEW_API
+void fm_mat4_inverse(fm_mat4_t *out, const fm_mat4_t *m);
+
+/**
+ * @brief Compute a normal matrix from an affine 4x4 matrix.
+ * @preview
+ *
+ * The resulting matrix can be used to transform face normal vectors while preserving
+ * their property of being perpendicular to the surface.
+ * In general, such a matrix is obtained by computing the transpose inverse of the
+ * matrix in question. If the input matrix is affine, however, it is possible to
+ * take a shortcut and only compute the transpose inverse of the upper left 3x3 part.
+ * Accordingly, this function only produces valid results if the input matrix is affine.
+ *
+ * Also note that computing the transpose inverse is technically only necessary if
+ * the input matrix represents any non-rigid transformations (scaling, shearing etc.).
+ * If your matrix is rigid (only translation and rotation), then the transpose inverse
+ * is in fact the same as the input and this function can be skipped.
+ * Additionally, if your matrix contains only uniform scaling (the same scale factor on all 3 axes)
+ * you may skip computing the inverse transpose as well, provided you re-normalize
+ * the normal vectors after transformation.
+ *
+ * @param[out] out The resulting matrix.
+ * @param[in] m The input matrix. Must be affine.
+ * @note The upper-left 3x3 part of the input must be invertible.
+ */
+LIBDRAGON_PREVIEW_API
+void fm_mat4_affine_to_normal_mat(fm_mat4_t *out, const fm_mat4_t *m);
+
+/**
+ * @brief Create a view matrix that represents a camera looking in a certain direction from some position.
+ * @preview
+ * @note The direction vector is expected to be normalized. The up vector must not be collinear with it.
+ */
+LIBDRAGON_PREVIEW_API
+void fm_mat4_look(fm_mat4_t *out, const fm_vec3_t *eye, const fm_vec3_t *dir, const fm_vec3_t *up);
+
+/**
+ * @brief Create a view matrix that represents a camera looking at a target point from some position.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+void fm_mat4_lookat(fm_mat4_t *out, const fm_vec3_t *eye, const fm_vec3_t *target, const fm_vec3_t *up);
+
+/**
+ * @brief Multiply a 3D vector by a 4x4 matrix by assuming 1 as the hypothetical 4th component of the vector.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_mat4_mul_vec3(fm_vec4_t *out, const fm_mat4_t *m, const fm_vec3_t *v)
+{
+ for (int i = 0; i < 4; i++)
+ {
+ out->v[i] = m->m[0][i] * v->x +
+ m->m[1][i] * v->y +
+ m->m[2][i] * v->z +
+ m->m[3][i];
+ }
+}
+
+/**
+ * @brief Multiply a 4D vector by a 4x4 matrix.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_mat4_mul_vec4(fm_vec4_t *out, const fm_mat4_t *m, const fm_vec4_t *v)
+{
+ fm_vec4_t tmp;
+ for (int i = 0; i < 4; i++)
+ {
+ tmp.v[i] = m->m[0][i] * v->x +
+ m->m[1][i] * v->y +
+ m->m[2][i] * v->z +
+ m->m[3][i] * v->w;
+ }
+ *out = tmp;
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+
+#ifdef __cplusplus
+
+ constexpr fm_vec3_t operator+(fm_vec3_t const& lhs, fm_vec3_t const& rhs) {
+ return {lhs.x + rhs.x, lhs.y + rhs.y, lhs.z + rhs.z};
+ }
+
+ constexpr fm_vec3_t operator-(fm_vec3_t const& lhs, fm_vec3_t const& rhs) {
+ return {lhs.x - rhs.x, lhs.y - rhs.y, lhs.z - rhs.z};
+ }
+
+ constexpr fm_vec3_t operator*(fm_vec3_t const& lhs, fm_vec3_t const& rhs) {
+ return {lhs.x * rhs.x, lhs.y * rhs.y, lhs.z * rhs.z};
+ }
+
+ constexpr fm_vec3_t operator/(fm_vec3_t const& lhs, fm_vec3_t const& rhs) {
+ return {lhs.x / rhs.x, lhs.y / rhs.y, lhs.z / rhs.z};
+ }
+
+ constexpr fm_vec3_t operator+(fm_vec3_t const& lhs, float rhs) {
+ return {lhs.x + rhs, lhs.y + rhs, lhs.z + rhs};
+ }
+
+ constexpr fm_vec3_t operator-(fm_vec3_t const& lhs, float rhs) {
+ return {lhs.x - rhs, lhs.y - rhs, lhs.z - rhs};
+ }
+
+ constexpr fm_vec3_t operator*(fm_vec3_t const& lhs, float rhs) {
+ return {lhs.x * rhs, lhs.y * rhs, lhs.z * rhs};
+ }
+
+ constexpr fm_vec3_t operator/(fm_vec3_t const& lhs, float rhs) {
+ return {lhs.x / rhs, lhs.y / rhs, lhs.z / rhs};
+ }
+
+ constexpr fm_vec3_t operator-(fm_vec3_t const& lhs) {
+ return {-lhs.x, -lhs.y, -lhs.z};
+ }
+
+ constexpr fm_vec3_t& operator+=(fm_vec3_t &lhs, fm_vec3_t const& rhs) {
+ lhs.x += rhs.x;
+ lhs.y += rhs.y;
+ lhs.z += rhs.z;
+ return lhs;
+ }
+
+ constexpr fm_vec3_t& operator+=(fm_vec3_t &lhs, float rhs) {
+ lhs.x += rhs;
+ lhs.y += rhs;
+ lhs.z += rhs;
+ return lhs;
+ }
+
+ constexpr fm_vec3_t& operator-=(fm_vec3_t &lhs, fm_vec3_t const& rhs) {
+ lhs.x -= rhs.x;
+ lhs.y -= rhs.y;
+ lhs.z -= rhs.z;
+ return lhs;
+ }
+
+ constexpr fm_vec3_t& operator-=(fm_vec3_t &lhs, float rhs) {
+ lhs.x -= rhs;
+ lhs.y -= rhs;
+ lhs.z -= rhs;
+ return lhs;
+ }
+
+ constexpr fm_vec3_t& operator*=(fm_vec3_t &lhs, fm_vec3_t const& rhs) {
+ lhs.x *= rhs.x;
+ lhs.y *= rhs.y;
+ lhs.z *= rhs.z;
+ return lhs;
+ }
+
+ constexpr fm_vec3_t& operator*=(fm_vec3_t &lhs, float rhs) {
+ lhs.x *= rhs;
+ lhs.y *= rhs;
+ lhs.z *= rhs;
+ return lhs;
+ }
+
+ constexpr fm_vec3_t& operator/=(fm_vec3_t &lhs, fm_vec3_t const& rhs) {
+ lhs.x /= rhs.x;
+ lhs.y /= rhs.y;
+ lhs.z /= rhs.z;
+ return lhs;
+ }
+
+ constexpr fm_vec3_t& operator/=(fm_vec3_t &lhs, float rhs) {
+ lhs.x /= rhs;
+ lhs.y /= rhs;
+ lhs.z /= rhs;
+ return lhs;
+ }
+
+ constexpr bool operator==(fm_vec3_t const& lhs, fm_vec3_t const& rhs) {
+ return lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z;
+ }
+
+ constexpr bool operator!=(fm_vec3_t const& lhs, fm_vec3_t const& rhs) {
+ return lhs.x != rhs.x || lhs.y != rhs.y || lhs.z != rhs.z;
+ }
+
+ constexpr bool operator==(fm_quat_t const& lhs, fm_quat_t const& rhs) {
+ return lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z && lhs.w == rhs.w;
+ }
+
+ constexpr bool operator!=(fm_quat_t const& lhs, fm_quat_t const& rhs) {
+ return lhs.x != rhs.x || lhs.y != rhs.y || lhs.z != rhs.z || lhs.w != rhs.w;
+ }
+
+ inline fm_quat_t operator*(fm_quat_t const& lhs, fm_quat_t const& rhs) {
+ fm_quat_t res{};
+ fm_quat_mul(&res, &lhs, &rhs);
+ return res;
+ }
+
+ inline fm_vec3_t operator*(fm_quat_t const& quat, fm_vec3_t const& vec) {
+ fm_vec3_t res{};
+ fm_vec3_rotate(&res, &vec, &quat);
+ return res;
+ }
+
+
+#endif
+
+#endif
\ No newline at end of file
diff --git a/include/fgeom2d.h b/include/fgeom2d.h
new file mode 100644
index 000000000..f92f8fe88
--- /dev/null
+++ b/include/fgeom2d.h
@@ -0,0 +1,482 @@
+/**
+ * @file fgeom2d.h
+ * @brief Structs and functions for common 2D geometry calculations.
+ * @ingroup fastmath
+ */
+
+#ifndef LIBDRAGON_FGEOM2D_H
+#define LIBDRAGON_FGEOM2D_H
+
+#include
+#include
+#include
+#include "preview.h"
+#include "fmath.h"
+#include "fgeom.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Two dimensional vector with single precision floating point components.
+ *
+ * The components can either be accessed via named fields (x, y),
+ * or by index using the v field.
+ */
+typedef union {
+ struct {
+ float x, y; ///< Named fields to access individual vector components directly.
+ };
+ float v[2]; ///< Array field to access the vector components via index.
+} fm_vec2_t;
+
+/**
+ * @brief 3x3 Matrix with single precision floating point components.
+ */
+typedef struct {
+ float m[3][3]; ///< Two-dimensional array that contains the matrix coefficients in column-major order.
+} fm_mat3_t;
+
+
+/******** VEC2 **********/
+
+/**
+ * @brief Negate a 2D vector.
+ * @preview
+ * @note This function still works if input and output are in the same memory location.
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_vec2_negate(fm_vec2_t *out, const fm_vec2_t *v)
+{
+ for (int i = 0; i < 2; i++) out->v[i] = -v->v[i];
+}
+
+/**
+ * @brief Add two 2D vectors component-wise.
+ * @preview
+ * @note This function still works if input and output are in the same memory location.
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_vec2_add(fm_vec2_t *out, const fm_vec2_t *a, const fm_vec2_t *b)
+{
+ for (int i = 0; i < 2; i++) out->v[i] = a->v[i] + b->v[i];
+}
+
+/**
+ * @brief Subtract two 2D vectors component-wise.
+ * @preview
+ * @note This function still works if input and output are in the same memory location.
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_vec2_sub(fm_vec2_t *out, const fm_vec2_t *a, const fm_vec2_t *b)
+{
+ for (int i = 0; i < 2; i++) out->v[i] = a->v[i] - b->v[i];
+}
+
+/**
+ * @brief Multiply two 2D vectors component-wise.
+ * @preview
+ * @note If you need the dot product, use #fm_vec2_dot instead.
+ * @note This function still works if input and output are in the same memory location.
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_vec2_mul(fm_vec2_t *out, const fm_vec2_t *a, const fm_vec2_t *b)
+{
+ for (int i = 0; i < 2; i++) out->v[i] = a->v[i] * b->v[i];
+}
+
+/**
+ * @brief Divide two 2D vectors component-wise.
+ * @preview
+ * @note This function still works if input and output are in the same memory location.
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_vec2_div(fm_vec2_t *out, const fm_vec2_t *a, const fm_vec2_t *b)
+{
+ for (int i = 0; i < 2; i++) out->v[i] = a->v[i] / b->v[i];
+}
+
+/**
+ * @brief Scale a 2D vector by a factor.
+ * @preview
+ * @note This function still works if input and output are in the same memory location.
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_vec2_scale(fm_vec2_t *out, const fm_vec2_t *a, float s)
+{
+ for (int i = 0; i < 2; i++) out->v[i] = a->v[i] * s;
+}
+
+/**
+ * @brief Compute the dot product of two 2D vectors.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline float fm_vec2_dot(const fm_vec2_t *a, const fm_vec2_t *b)
+{
+ return a->x * b->x + a->y * b->y;
+}
+
+/**
+ * @brief Compute the square magnitude of a 2D vector.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline float fm_vec2_len2(const fm_vec2_t *a)
+{
+ return fm_vec2_dot(a, a);
+}
+
+/**
+ * @brief Compute the magnitude of a 2D vector.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline float fm_vec2_len(const fm_vec2_t *a)
+{
+ return sqrtf(fm_vec2_len2(a));
+}
+
+/**
+ * @brief Compute the square distance between two 2D vectors.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline float fm_vec2_distance2(const fm_vec2_t *a, const fm_vec2_t *b)
+{
+ fm_vec2_t diff;
+ fm_vec2_sub(&diff, a, b);
+ return fm_vec2_len2(&diff);
+}
+
+/**
+ * @brief Compute the distance between two 2D vectors.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline float fm_vec2_distance(const fm_vec2_t *a, const fm_vec2_t *b)
+{
+ return sqrtf(fm_vec2_distance2(a, b));
+}
+
+/**
+ * @brief Normalize a 2D vector.
+ * @preview
+ * @note This function still works if input and output are in the same memory location.
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_vec2_norm(fm_vec2_t *out, const fm_vec2_t *a)
+{
+ float len = fm_vec2_len(a);
+ if (len < FM_EPSILON) {
+ *out = (fm_vec2_t){};
+ return;
+ }
+ *out = (fm_vec2_t){{ a->x / len, a->y / len }};
+}
+
+/**
+ * @brief Linearly interpolate between two 2D vectors.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_vec2_lerp(fm_vec2_t *out, const fm_vec2_t *a, const fm_vec2_t *b, float t)
+{
+ for (int i = 0; i < 2; i++) out->v[i] = a->v[i] + (b->v[i] - a->v[i]) * t;
+}
+
+/******** MAT3 **********/
+
+/**
+ * @brief Create a 3x3 identity matrix.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_mat3_identity(fm_mat3_t *out)
+{
+ *out = (fm_mat3_t){};
+ out->m[0][0] = 1;
+ out->m[1][1] = 1;
+ out->m[2][2] = 1;
+}
+
+/**
+ * @brief Apply scale to a 3x3 matrix.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_mat3_scale(fm_mat3_t *out, const fm_vec2_t *scale)
+{
+ for (int i=0; i<3; i++) out->m[i][0] *= scale->x;
+ for (int i=0; i<3; i++) out->m[i][1] *= scale->y;
+}
+
+/**
+ * @brief Apply translation to a 3x3 matrix.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_mat3_translate(fm_mat3_t *out, const fm_vec2_t *translate)
+{
+ for (int i=0; i<2; i++) out->m[2][i] += translate->v[i];
+}
+
+/**
+ * @brief Apply a counterclockwise rotation to a 3x3 matrix.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+void fm_mat3_rotate(fm_mat3_t *out, float rotation);
+
+/**
+ * @brief Create a 3x3 affine transformation matrix from scale, rotation and translation.
+ * @preview
+ *
+ * The rotation is accepted as a counterclockwise angle in radians.
+ * It creates an equivalent matrix to multiplying an identity matrix by a scale,
+ * a rotation, and a translation matrix in that order.
+ */
+LIBDRAGON_PREVIEW_API
+void fm_mat3_from_srt(fm_mat3_t *out, const fm_vec2_t *scale, float rotation, const fm_vec2_t *translate);
+
+/**
+ * @brief Create a 3x3 affine transformation matrix from rotation, scale, and translation.
+ * @preview
+ *
+ * The rotation is accepted as a counterclockwise angle in radians.
+ * It creates an equivalent matrix to multiplying an identity matrix by a rotation,
+ * a scale, and a translation matrix in that order.
+ */
+LIBDRAGON_PREVIEW_API
+void fm_mat3_from_rst(fm_mat3_t *out, float rotation, const fm_vec2_t *scale, const fm_vec2_t *translate);
+
+/**
+ * @brief Create a 3x3 rigid transformation matrix from rotation and translation.
+ * @preview
+ *
+ * The rotation is accepted as an angle in radians.
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_mat3_from_rt(fm_mat3_t *out, float rotation, const fm_vec2_t *translate)
+{
+ const fm_vec2_t scale = {{1, 1}};
+ fm_mat3_from_srt(out, &scale, rotation, translate);
+}
+
+/**
+ * @brief Create a 3x3 translation matrix.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_mat3_from_translation(fm_mat3_t *out, const fm_vec2_t *translate)
+{
+ fm_mat3_identity(out);
+ fm_mat3_translate(out, translate);
+}
+
+/**
+ * @brief Create a 3x3 rotation matrix from angle.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_mat3_from_rotation(fm_mat3_t *out, float theta)
+{
+ const fm_vec2_t translate = {{0, 0}};
+ fm_mat3_from_rt(out, theta, &translate);
+}
+
+/**
+ * @brief Create a 3x3 scale matrix.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_mat3_from_scale(fm_mat3_t *out, const fm_vec2_t *scale)
+{
+ fm_mat3_identity(out);
+ fm_mat3_scale(out, scale);
+}
+
+/**
+ * @brief Multiply two 3x3 matrices.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_mat3_mul(fm_mat3_t *out, const fm_mat3_t *a, const fm_mat3_t *b)
+{
+ fm_mat3_t tmp;
+ for (int i = 0; i < 3; i++)
+ {
+ for (int j = 0; j < 3; j++)
+ {
+ tmp.m[i][j] = a->m[0][j] * b->m[i][0] +
+ a->m[1][j] * b->m[i][1] +
+ a->m[2][j] * b->m[i][2];
+ }
+ }
+ *out = tmp;
+}
+
+/**
+ * @brief Transpose a 3x3 matrix.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_mat3_transpose(fm_mat3_t *out, const fm_mat3_t *m)
+{
+ for (int i = 0; i < 3; i++)
+ {
+ for (int j = 0; j < 3; j++)
+ {
+ out->m[i][j] = m->m[j][i];
+ }
+ }
+}
+
+/**
+ * @brief Compute the determinant of a 3x3 matrix.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+float fm_mat3_det(const fm_mat3_t *m);
+
+/**
+ * @brief Compute the inverse of a 3x3 matrix.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+void fm_mat3_inverse(fm_mat3_t *out, const fm_mat3_t *m);
+
+/**
+ * @brief Multiply a 2D vector by a 3x3 matrix by assuming 1 as the hypothetical 3rd component of the vector.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_mat3_mul_vec2(fm_vec3_t *out, const fm_mat3_t *m, const fm_vec2_t *v)
+{
+ for (int i = 0; i < 3; i++)
+ {
+ out->v[i] = m->m[0][i] * v->x +
+ m->m[1][i] * v->y +
+ m->m[2][i];
+ }
+}
+
+/**
+ * @brief Multiply a 3D vector by a 3x3 matrix.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline void fm_mat3_mul_vec3(fm_vec3_t *out, const fm_mat3_t *m, const fm_vec3_t *v)
+{
+ fm_vec3_t tmp;
+ for (int i = 0; i < 3; i++)
+ {
+ tmp.v[i] = m->m[0][i] * v->x +
+ m->m[1][i] * v->y +
+ m->m[2][i] * v->z;
+ }
+ *out = tmp;
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#ifdef __cplusplus
+
+constexpr fm_vec2_t operator+(fm_vec2_t const& lhs, fm_vec2_t const& rhs) {
+ return {lhs.x + rhs.x, lhs.y + rhs.y};
+}
+
+constexpr fm_vec2_t operator-(fm_vec2_t const& lhs, fm_vec2_t const& rhs) {
+ return {lhs.x - rhs.x, lhs.y - rhs.y};
+}
+
+constexpr fm_vec2_t operator*(fm_vec2_t const& lhs, fm_vec2_t const& rhs) {
+ return {lhs.x * rhs.x, lhs.y * rhs.y};
+}
+
+constexpr fm_vec2_t operator/(fm_vec2_t const& lhs, fm_vec2_t const& rhs) {
+ return {lhs.x / rhs.x, lhs.y / rhs.y};
+}
+
+constexpr fm_vec2_t operator+(fm_vec2_t const& lhs, float rhs) {
+ return {lhs.x + rhs, lhs.y + rhs};
+}
+
+constexpr fm_vec2_t operator-(fm_vec2_t const& lhs, float rhs) {
+ return {lhs.x - rhs, lhs.y - rhs};
+}
+
+constexpr fm_vec2_t operator*(fm_vec2_t const& lhs, float rhs) {
+ return {lhs.x * rhs, lhs.y * rhs};
+}
+
+constexpr fm_vec2_t operator/(fm_vec2_t const& lhs, float rhs) {
+ return {lhs.x / rhs, lhs.y / rhs};
+}
+
+constexpr fm_vec2_t operator-(fm_vec2_t const& lhs) {
+ return {-lhs.x, -lhs.y};
+}
+
+constexpr fm_vec2_t& operator+=(fm_vec2_t &lhs, fm_vec2_t const& rhs) {
+ lhs.x += rhs.x;
+ lhs.y += rhs.y;
+ return lhs;
+}
+
+constexpr fm_vec2_t& operator+=(fm_vec2_t &lhs, float rhs) {
+ lhs.x += rhs;
+ lhs.y += rhs;
+ return lhs;
+}
+
+constexpr fm_vec2_t& operator-=(fm_vec2_t &lhs, fm_vec2_t const& rhs) {
+ lhs.x -= rhs.x;
+ lhs.y -= rhs.y;
+ return lhs;
+}
+
+constexpr fm_vec2_t& operator-=(fm_vec2_t &lhs, float rhs) {
+ lhs.x -= rhs;
+ lhs.y -= rhs;
+ return lhs;
+}
+
+constexpr fm_vec2_t& operator*=(fm_vec2_t &lhs, fm_vec2_t const& rhs) {
+ lhs.x *= rhs.x;
+ lhs.y *= rhs.y;
+ return lhs;
+}
+
+constexpr fm_vec2_t& operator*=(fm_vec2_t &lhs, float rhs) {
+ lhs.x *= rhs;
+ lhs.y *= rhs;
+ return lhs;
+}
+
+constexpr fm_vec2_t& operator/=(fm_vec2_t &lhs, fm_vec2_t const& rhs) {
+ lhs.x /= rhs.x;
+ lhs.y /= rhs.y;
+ return lhs;
+}
+
+constexpr fm_vec2_t& operator/=(fm_vec2_t &lhs, float rhs) {
+ lhs.x /= rhs;
+ lhs.y /= rhs;
+ return lhs;
+}
+
+constexpr bool operator==(fm_vec2_t const& lhs, fm_vec2_t const& rhs) {
+ return lhs.x == rhs.x && lhs.y == rhs.y;
+}
+
+constexpr bool operator!=(fm_vec2_t const& lhs, fm_vec2_t const& rhs) {
+ return lhs.x != rhs.x || lhs.y != rhs.y;
+}
+
+#endif
+
+#endif
\ No newline at end of file
diff --git a/include/flashram.h b/include/flashram.h
new file mode 100644
index 000000000..4e47941ba
--- /dev/null
+++ b/include/flashram.h
@@ -0,0 +1,166 @@
+/**
+ * @file flashram.h
+ * @author Christopher Bonhage
+ * @brief FlashRAM access functions for N64 cartridges
+ * @ingroup flashram
+ */
+
+#ifndef LIBDRAGON_FLASHRAM_H
+#define LIBDRAGON_FLASHRAM_H
+
+#include "dma.h"
+#include
+#include
+#include
+
+/**
+ * @defgroup flashram FlashRAM
+ * @ingroup peripherals
+ * @brief FlashRAM save storage for N64 cartridges
+ *
+ * This module provides access to the FlashRAM save chip found in some N64
+ * cartridges (a 1 Mibit / 128 KiB Macronix- or Matsushita-family NOR flash).
+ * Unlike SRAM, which is a flat, byte-addressable memory (see @ref sram.h),
+ * FlashRAM is driven through a small command state machine on the PI bus and can
+ * only be erased in 16 KiB sectors and programmed in 128-byte pages.
+ *
+ * The API is an SRAM-like byte-range interface (#flashram_read /
+ * #flashram_write) that accepts arbitrary offsets and lengths. #flashram_write
+ * performs a read-modify-write over the affected sectors so that data outside
+ * the written range (but within the same erase sector) is preserved.
+ *
+ * Call #flashram_init once at boot before using any other function, and set
+ * `N64_ROM_SAVETYPE = flashram` in your Makefile so emulators and flashcarts
+ * configure the correct save chip.
+ *
+ * @{
+ */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define FLASHRAM_ADDRESS 0x08000000 ///< Base address of FlashRAM in PI address space (shared with SRAM domain).
+
+/**
+ * @brief FlashRAM chip layout, expressed as bits-per-component.
+ *
+ * Encoding each dimension as a power of two describes both byte- and
+ * word-indexed parts uniformly and generalizes to other page/sector sizes.
+ * Some are word-indexed (@p unit_bits = 1): a PI-bus address selects a
+ * 16-bit word, so a logical byte offset is halved to reach the right word.
+ * Byte-indexed parts (@p unit_bits = 0) address individual bytes, like
+ * SRAM. The layout is a fixed property of the silicon, looked up from its
+ * silicon ID.
+ *
+ * Derived quantities (all powers of two):
+ * - unit size (bytes) = 1 << unit_bits
+ * - page size (bytes) = 1 << (unit_bits + offset_bits)
+ * - pages per sector = 1 << page_bits
+ * - sector size (bytes) = 1 << (unit_bits + offset_bits + page_bits)
+ * - sectors per chip = 1 << sector_bits
+ * - total size (bytes) = 1 << (unit_bits + offset_bits + page_bits + sector_bits)
+ * - DMA read boundary (bytes, not to be crossed by a single transfer)
+ * = 1 << (unit_bits + offset_bits + read_page_bits)
+ */
+typedef struct
+{
+ uint8_t unit_bits; ///< log2(bytes per addressable unit): 0 = byte-indexed, 1 = word-indexed.
+ uint8_t offset_bits; ///< log2(addressable units per page).
+ uint8_t page_bits; ///< log2(pages per sector).
+ uint8_t sector_bits; ///< log2(sectors per chip).
+ uint8_t read_page_bits; ///< log2(pages) of the DMA read boundary a single transfer must not cross.
+} flashram_layout_t;
+
+/**
+ * @brief Identity and layout of the detected FlashRAM chip.
+ *
+ * Filled in by #flashram_init. @p layout describes the addressing convention
+ * and geometry as bits-per-component; @p total_size / @p sector_size /
+ * @p page_size and the derived counts are that geometry expanded into bytes for
+ * convenience.
+ */
+typedef struct
+{
+ uint32_t type_id; ///< FLASH_TYPE_ID (expected 0x11118001).
+ uint16_t manufacturer_id; ///< Manufacturer ID (e.g. 0x00C2 Macronix, 0x0032 Matsushita).
+ uint16_t device_id; ///< Device ID (identifies the specific part).
+ flashram_layout_t layout; ///< Chip layout: byte/word addressing and geometry.
+ const char* name; ///< Human-readable model name ("unknown" if not in the table).
+ size_t total_size; ///< Total capacity in bytes (derived from @p layout).
+ size_t sector_size; ///< Erase-sector size in bytes (derived from @p layout).
+ size_t page_size; ///< Program-page size in bytes (derived from @p layout).
+ unsigned int num_sectors; ///< Number of erase sectors (derived from @p layout).
+ unsigned int num_pages; ///< Number of program pages (derived from @p layout).
+} flashram_info_t;
+
+/**
+ * @brief Initialize the FlashRAM subsystem and detect the chip
+ * @preview
+ *
+ * Configures the PI DOM2 registers, then reads the chip's silicon ID to check
+ * for FlashRAM and, when present, look up the model: its name and (crucially)
+ * its layout, which is cached and drives every subsequent read/write. Leaves the
+ * chip in read mode. Must be called before any other FlashRAM function.
+ *
+ * Detection is non-destructive (it does not write to the save area). The layout
+ * must be known for reads/writes to address the array correctly on word-indexed
+ * parts, which is why it is resolved here rather than in a separate step.
+ *
+ * @param timings Optional PI DOM2 (#pi_dom_timings_t) bus timings; pass NULL for
+ * the standard defaults (latency 0x05, pulse 0x0C, page size 0x0F,
+ * release 0x02). @p page_size must stay at its maximum (0x0F) so
+ * the PI never auto-splits DMAs and the driver can split them
+ * itself -- required for word-indexed parts.
+ * @param info Optional out-parameter; when non-NULL and FlashRAM is present,
+ * it is filled with the chip identity and layout.
+ * @return true if FlashRAM is present, false otherwise.
+ */
+LIBDRAGON_PREVIEW_API
+bool flashram_init(const pi_dom_timings_t* timings, flashram_info_t* info);
+
+/**
+ * @brief Read data from FlashRAM
+ * @preview
+ *
+ * Reads a byte range from FlashRAM into @p dst. Any @b even offset and any
+ * length are accepted (an odd offset or out-of-range range asserts)
+ *
+ * @param dst Destination buffer to store the read data.
+ * @param offset Even byte offset in FlashRAM to read from (0 to the detected total_size - 1).
+ * @param len Number of bytes to read.
+ * @return Number of bytes read (equal to @p len). Invalid arguments assert.
+ */
+LIBDRAGON_PREVIEW_API
+int flashram_read(void* dst, size_t offset, size_t len);
+
+/**
+ * @brief Write data to FlashRAM (read-modify-write)
+ * @preview
+ *
+ * Writes a byte range to FlashRAM from @p src. Because FlashRAM can only be
+ * erased a whole 16 KiB sector at a time, this performs a read-modify-write on
+ * every sector the range touches: sectors that are only partially overwritten
+ * are read back first so their untouched bytes are preserved. Pages whose final
+ * content is entirely erased (all 0xFF) are skipped.
+ *
+ * This is a blocking operation and can take up to a few hundred milliseconds
+ * per touched sector on real hardware (sector erase plus up to 128 page
+ * programs).
+ *
+ * @param src Source buffer containing the data to write.
+ * @param offset Byte offset in FlashRAM to write to (0 to the detected total_size - 1).
+ * @param len Number of bytes to write.
+ * @return Number of bytes written, or a negative value if an erase/program fails
+ * on the hardware. Invalid arguments assert.
+ */
+LIBDRAGON_PREVIEW_API
+int flashram_write(const void* src, size_t offset, size_t len);
+
+#ifdef __cplusplus
+}
+#endif
+
+/** @} */ /* flashram */
+
+#endif
diff --git a/include/fmath.h b/include/fmath.h
index 68b2b8e78..40d8613fe 100644
--- a/include/fmath.h
+++ b/include/fmath.h
@@ -59,19 +59,41 @@
#ifndef __LIBDRAGON_FMATH_H
#define __LIBDRAGON_FMATH_H
+
#include
#include
#include
+#include "preview.h"
#ifdef __cplusplus
extern "C" {
#endif
+/**
+ * @brief Error constant for approximate comparisons.
+ */
+#define FM_EPSILON 1e-4f
+
+/**
+ * @brief Single precision pi constant.
+ */
+#define FM_PI 3.14159265358979f
+
+/**
+ * @brief Convert degrees to radians.
+ */
+#define FM_DEG2RAD(deg) ((deg) * (FM_PI / 180.0f))
+
+/**
+ * @brief Convert radians to degrees.
+ */
+#define FM_RAD2DEG(rad) ((rad) * (180.0f / FM_PI))
+
/** @brief Reinterpret the bits composing a float as a int32.
*
* This version is type-punning safe and produces optimal code when optimizing.
**/
-#define BITCAST_F2I(f) ({ int32_t i; memcpy(&i, &f, 4); i; })
+#define BITCAST_F2I(f) ({ int32_t i; float _f = (f); memcpy(&i, &_f, 4); i; })
/** @brief Reinterpret the bits composing a int32 as a float.
*
@@ -95,7 +117,7 @@ static inline float fm_truncf(float x) {
}
/**
- * @brief Faster version of floorf
+ * @brief Faster version of ceilf
*
* Optimized version using the MIPS ceil.w.s instruction.
*/
@@ -118,6 +140,20 @@ static inline float fm_floorf(float x) {
return y;
}
+/**
+ * @brief Faster version of roundf
+ * @preview
+ *
+ * Optimized version using the MIPS round.w.s instruction.
+ */
+ LIBDRAGON_PREVIEW_API
+ static inline float fm_roundf(float x) {
+ float yint, y;
+ __asm ("round.w.s %0,%1" : "=f"(yint) : "f"(x));
+ __asm ("cvt.s.w %0,%1" : "=f"(y) : "f"(yint));
+ return y;
+}
+
/**
* @brief Faster version of fmodf
*
@@ -213,6 +249,66 @@ void fm_sincosf(float x, float *sin, float *cos);
*/
float fm_atan2f(float y, float x);
+/**
+ * @brief Approximation of exp(x) with a relative error <3%.
+ * @preview
+ * This is several times faster than exp(x). The implementation uses a
+ * method by Nicole Schraudolph.
+ *
+ * Note that this function does not have bounds check
+ * and will overflow if x is not in ~(-85;85) range.
+ * It is recommended to clamp x and do fm_exp(CLAMP(x, -85, 85))
+ *
+ */
+LIBDRAGON_PREVIEW_API
+float fm_expf(float x);
+
+/**
+ * @brief Faster version of exp2f (2^x).
+ * @preview
+ *
+ * Splits @p x into an integer power of two (via the IEEE754 exponent field)
+ * and a fractional part evaluated with a degree-5 minimax polynomial on
+ * `[0, 1]`. Relative error is about `1e-5` — several times more accurate
+ * than #fm_expf, and far cheaper than newlib `exp2f` / `powf`.
+ *
+ * No bounds check: overflows if @p x is outside roughly `(-126, 128)`.
+ */
+LIBDRAGON_PREVIEW_API
+float fm_exp2f(float x);
+
+/**
+ * @brief Linearly interpolate between two scalar values.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+inline float fm_lerp(float a, float b, float t)
+{
+ return a + (b - a) * t;
+}
+
+/**
+ * @brief Linearly interpolate between two angles, using the shortest path.
+ * @preview
+ *
+ * @param a The start angle in radians
+ * @param b The end angle in radians
+ * @param t The interpolation factor
+ * @return The interpolated angle.
+ */
+LIBDRAGON_PREVIEW_API
+float fm_lerp_angle(float a, float b, float t);
+
+/**
+ * @brief Wrap an angle into [0..pi*2] range.
+ * @preview
+ *
+ * @param[in] angle An angle in radians.
+ * @return The wrapped angle.
+ */
+LIBDRAGON_PREVIEW_API
+float fm_wrap_angle(float angle);
+
#ifdef LIBDRAGON_FAST_MATH
#define truncf(x) fm_truncf(x)
#define floorf(x) fm_floorf(x)
@@ -230,6 +326,8 @@ float fm_atan2f(float y, float x);
#define cosf(x) (__builtin_constant_p(x) ? cosf(x) : fm_cosf(x))
#define sincosf(x,s,c) (__builtin_constant_p(x) ? sincosf(x,s,c) : fm_sincosf(x,s,c))
#define atan2f(y, x) ((__builtin_constant_p(x) && __builtin_constant_p(y)) ? atan2f(y, x) : fm_atan2f(y, x))
+ #define expf(x) (__builtin_constant_p(x) ? expf(x) : fm_expf(x))
+ #define exp2f(x) (__builtin_constant_p(x) ? exp2f(x) : fm_exp2f(x))
#endif
#ifdef __cplusplus
diff --git a/include/fmv.h b/include/fmv.h
new file mode 100644
index 000000000..219b6fe21
--- /dev/null
+++ b/include/fmv.h
@@ -0,0 +1,223 @@
+/**
+ * @file fmv.h
+ * @author Giovanni Bajo
+ * @brief Full-motion video playback (high-level API)
+ *
+ * This module provides a super-high-level API to play full-motion videos with
+ * audio. It builds upon the video library (video.h) and audio streaming library
+ * (wav64.h) to provide a one-function-does-it-all API to play a video.
+ *
+ * If you just want to drop a FMV in your application (eg: for a logo screen),
+ * this is the easiest way to do it.
+ */
+
+#ifndef LIBDRAGON_VIDEO_FMV_H
+#define LIBDRAGON_VIDEO_FMV_H
+
+#include
+#include "preview.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+///@cond
+typedef struct video_s video_t;
+typedef struct video_info_s video_info_t;
+typedef struct yuv_blitter_s yuv_blitter_t;
+typedef struct wav64_s wav64_t;
+typedef struct subtitles_s subtitles_t;
+typedef struct subrenderer_s subrenderer_t;
+///@endcond
+
+/** @brief Structure to control FMV playback */
+typedef struct fmv_control_s {
+ /** Video track */
+ video_t *video;
+ /** Audio track */
+ wav64_t *audio;
+ /** Subtitle track */
+ subtitles_t *subs;
+ /** Pause/unpause playback */
+ void (*pause)(struct fmv_control_s *ctrl, bool pause);
+ /** Stop playback */
+ void (*stop)(struct fmv_control_s *ctrl);
+ /** Seek to a specific frame */
+ int (*seek_frame)(struct fmv_control_s *ctrl, int frame_idx, bool exact);
+ /** Seek to a specific time (in seconds) */
+ float (*seek_time)(struct fmv_control_s *ctrl, float time_sec, bool exact);
+} fmv_control_t;
+
+/** @brief FMV playback parameters */
+typedef struct fmv_parms_s {
+ /** @brief Disable audio playback (even though an audio file is present) */
+ bool disable_audio;
+ /** @brief Disable subtitle rendering (even though a subtitle file is present) */
+ bool disable_subtitles;
+ /**
+ * @brief Disable configuration of display mode.
+ *
+ * By default, #fmv_play will call #display_init to configure a "perfect",
+ * resolution for the video playback. This will be a custom VI resolution
+ * to perfectly match the video resolution *and* aspect ratio, so that any
+ * required letterboxing will be be performed at zero cost by VI itself.
+ *
+ * Alternatively, #fmv_play can also render the video over an existing display
+ * configuration. This is mostly useful for cases where you need a smooth
+ * transition between the video playback and some other graphics, as calling
+ * display_close() will always incur in a black flash (because of framebuffer
+ * deallocation and reallocation).
+ *
+ * In this case, the video will be automatically scaled to the display size,
+ * and aspect ratio will be preserved by actively drawing black bars into
+ * the framebuffers.
+ *
+ * Notice that if you are using a 16bpp display, the best results quality-wise
+ * are achieved with #FILTERS_RESAMPLE_ANTIALIAS_DEDITHER.
+ */
+ bool disable_display_init;
+ /**
+ * @brief Disable frame skipping to keep audio in sync.
+ *
+ * Note that disabling this can cause video and audio to go out of sync,
+ * if the video player can't keep up with the audio.
+ */
+ bool disable_frame_skipping;
+
+ /**
+ * @brief Filename of the audio track to play alongside the video.
+ *
+ * If not specified, the video player will try to find an audio track
+ * with the same name as the video file, but with a .wav64 extension.
+ */
+ const char *audio_fn;
+
+ /**
+ * @brief Subtitle track filename to play alongside the video.
+ *
+ * If not specified, the video player will try to find a subtitle track
+ * with the same name as the video file, but with a .sub64 extension.
+ */
+ const char *subtitles_fn;
+
+ /** The mixer channel to use for audio playback (default: 0) */
+ int audio_mixer_channel;
+
+ /** @brief Enable CRT overscan margins (only if display_init is not disabled) */
+ bool crt_margin;
+
+ /** @brief Continue playback from the start when the end of the video is reached */
+ bool loop;
+
+ /**
+ * @brief Subtitle renderer to use.
+ *
+ * This parameter allows to specify a custom subtitle renderer, that can
+ * be used to customise how subtitles are rendered on screen. There are
+ * two built-in renderers:
+ *
+ * * RDPQ rendered (#subrenderer_create_rdpq) that draws the subtitles over
+ * the video using rdpq for high-quality text rendering.
+ * * EIA-608 renderer (#subrenderer_create_eia608) that encodes subtitles
+ * using the EIA-608 standard for closed captions (on NTSC TVs).
+ *
+ * If this parameter is NULL, a default RDPQ renderer will be created
+ * and used, with a default builtin tool.
+ *
+ * @note Ownership of the renderer is transferred to the FMV player, that
+ * will free it at the end of playback.
+ */
+ subrenderer_t *sub_renderer;
+
+ /**
+ * @brief Callback function to draw OSD on top of the video
+ *
+ * This callback function, if provided, will be called once per frame,
+ * after the video frame has been drawn but before the framebuffer
+ * is presented on the screen.
+ *
+ * This allows to draw an on-screen display (OSD) on top of the video, for
+ * instance to show playback controls, subtitles, or other information.
+ *
+ * It also allows per-frame input control (eg: to implement pausing or
+ * skipping);
+ *
+ * @param osd_ctx Context pointer passed to the OSD callback
+ * @param frame_idx Index of the current frame being played
+ * @param time_sec Current playback time in seconds
+ * @param ctrl Pointer to a control structure that can be used
+ * to control playback (pause, stop, seek, etc.)
+ */
+ void (*osd_callback)(void *osd_ctx, int frame_idx, float time_sec, fmv_control_t *ctrl);
+ void *osd_ctx; ///< Context pointer passed to the OSD callback
+
+ /**
+ * @brief Custom YUV blitter factory (optional)
+ *
+ * If provided, replaces the default #yuv_blitter_new_fmv blitter used by
+ * #fmv_play. This allows customizing how video frames are scaled and drawn
+ * onto the framebuffer (eg: for mirroring or custom letterboxing).
+ *
+ * @param osd_ctx Value of #fmv_parms_t::osd_ctx
+ * @param info Video metadata from the opened stream
+ */
+ yuv_blitter_t (*create_yuv_blitter)(void *osd_ctx, video_info_t *info);
+
+ /**
+ * @brief Number of pictures to decode ahead of the current picture.
+ *
+ * Decoding time can vary significantly between pictures. This parameter
+ * controls how many decoded pictures the player may buffer ahead, allowing
+ * faster pictures to compensate for slower ones and reducing the risk of
+ * playback stalls. Higher values can improve playback smoothness at the cost
+ * of increased memory usage.
+ *
+ * If 0, defaults to 4 on a 4 MiB N64 and 8 with an Expansion Pak.
+ */
+ int decode_ahead_pics;
+} fmv_parms_t;
+
+
+/**
+ * @brief Play a full-motion video from a video file
+ * @preview
+ *
+ * This function plays a full-motion video from a video file, along with its
+ * audio track if present. It takes care of everything: setting up the
+ * display subsystem, opening the video and audio files, decoding and
+ * rendering the video frames, playing back the audio, and synchronizing
+ * everything. The function blocks until the video playback is complete (or is
+ * aborted via the control interface, see below).
+ *
+ * Playback can be customized via the #fmv_parms_t structure. In particular,
+ * it is possible to register a callback function (#fmv_parms_t::osd_callback)
+ * that is called once per frame, allowing to draw an on-screen display (OSD)
+ * on top of the video, or to control playback (pause, stop, seek, etc.). For
+ * instance, this can be used to implement skipping the video when a button is
+ * pressed, like in this example:
+ *
+ * \@code{.c}
+ * void my_osd_callback(void *ctx, int frame_idx, float time_sec, fmv_control_t *ctrl) {
+ * joypad_poll();
+ * joypad_buttons_t btn = joypad_get_buttons_pressed(JOYPAD_PORT_1);
+ * if (btn.start) ctrl->stop(ctrl);
+ * }
+ *
+ * fmv_parms_t parms = {
+ * .osd_callback = my_osd_callback,
+ * };
+ *
+ * fmv_play("rom:/intro.h264", &parms);
+ * \@endcode
+ *
+ * @param filename Filename of the video file to play (including filesystem prefix)
+ * @param parms Additional parameters to customize playback (can be NULL)
+ */
+LIBDRAGON_PREVIEW_API
+void fmv_play(const char *filename, const fmv_parms_t *parms);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/graphics.h b/include/graphics.h
index e279f311b..53a4abcf1 100644
--- a/include/graphics.h
+++ b/include/graphics.h
@@ -95,6 +95,46 @@ _Static_assert(sizeof(color_t) == 4, "invalid sizeof for color_t");
/** @brief Create a #color_t from the R,G,B,A components in the RGBA32 range (0-255). */
#define RGBA32(rx,gx,bx,ax) ((color_t){.r=rx, .g=gx, .b=bx, .a=ax})
+/** @brief Create a #color_t from the R,G,B,A components in the linear 16bit color space (that is: RGB in 0-31 squared, A in 0-1), use with #GAMMA_CORRECT display. */
+#define LINEAR16(rx, gx, bx, ax) \
+ (__builtin_constant_p(rx) && \
+ __builtin_constant_p(gx) && \
+ __builtin_constant_p(bx) && \
+ __builtin_constant_p(ax) ? \
+ ((color_t){ \
+ .r = ((((rx) << 3) | ((rx) >> 3))*(((rx) << 3) | ((rx) >> 3))) >> 8, \
+ .g = ((((gx) << 3) | ((gx) >> 3))*(((gx) << 3) | ((gx) >> 3))) >> 8, \
+ .b = ((((bx) << 3) | ((bx) >> 3))*(((bx) << 3) | ((bx) >> 3))) >> 8, \
+ .a = (ax) ? 0xFF : 0 \
+ }) : \
+ ( __extension__ ({ \
+ int rx0 = rx, gx0 = gx, bx0 = bx; \
+ int rx1 = (rx0<<3)|(rx0>>3), gx1 = (gx0<<3)|(gx0>>3), bx1 = (bx0<<3)|(bx0>>3); \
+ rx1 *= rx1; gx1 *= gx1; bx1 *= bx1; \
+ rx1 >>= 8; gx1 >>= 8; bx1 >>= 8; \
+ (color_t){.r=rx1, .g=gx1, .b=bx1, .a=ax ? 0xFF : 0}; \
+ }) ) \
+ )
+/** @brief Create a #color_t from the R,G,B,A components in the linear color space (0-255 squared), use with #GAMMA_CORRECT display. */
+#define LINEAR32(rx, gx, bx, ax) \
+ (__builtin_constant_p(rx) && \
+ __builtin_constant_p(gx) && \
+ __builtin_constant_p(bx) && \
+ __builtin_constant_p(ax) ? \
+ ((color_t){ \
+ .r = (((rx)*(rx)) >> 8), \
+ .g = (((gx)*(gx)) >> 8), \
+ .b = (((bx)*(bx)) >> 8), \
+ .a = ax \
+ }) : \
+ ( __extension__ ({ \
+ int rx1 = rx, gx1 = gx, bx1 = bx; \
+ rx1 *= rx1; gx1 *= gx1; bx1 *= bx1; \
+ rx1 >>= 8; gx1 >>= 8; bx1 >>= 8; \
+ (color_t){.r=rx1, .g=gx1, .b=bx1, .a=ax}; \
+ }) ) \
+ )
+
/** @brief Convert a #color_t to the 16-bit packed format used by a #FMT_RGBA16 surface (RGBA 5551) */
inline uint16_t color_to_packed16(color_t c) {
return (((int)c.r >> 3) << 11) | (((int)c.g >> 3) << 6) | (((int)c.b >> 3) << 1) | (c.a >> 7);
@@ -102,7 +142,7 @@ inline uint16_t color_to_packed16(color_t c) {
/** @brief Convert a #color_t to the 32-bit packed format used by a #FMT_RGBA32 surface (RGBA 8888) */
inline uint32_t color_to_packed32(color_t c) {
- return *(uint32_t*)&c;
+ return (c.r << 24) | (c.g << 16) | (c.b << 8) | (c.a << 0);
}
/** @brief Create a #color_t from the 16-bit packed format used by a #FMT_RGBA16 surface (RGBA 5551) */
inline color_t color_from_packed16(uint16_t c) {
diff --git a/include/h264.h b/include/h264.h
new file mode 100644
index 000000000..a1bfba533
--- /dev/null
+++ b/include/h264.h
@@ -0,0 +1,31 @@
+/**
+ * @file h264.h
+ * @author Giovanni Bajo
+ * @brief RSP-accelerated H.264 video player
+ */
+#ifndef LIBDRAGON_H264_H
+#define LIBDRAGON_H264_H
+
+#include "video.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief H.264 video codec
+ *
+ * Register this codec via #video_register_codec to enable H.264 video playback
+ * using the video.h API.
+ *
+ * This codec supports raw H264 elementary streams with no container. The supported
+ * extension is `.h264`. Do not use this codec with container formats such as MP4,
+ * MKV, AVI, etc.
+ */
+extern video_codec_t h264_codec;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
\ No newline at end of file
diff --git a/include/interrupt.h b/include/interrupt.h
index d71a690f5..e86e78870 100644
--- a/include/interrupt.h
+++ b/include/interrupt.h
@@ -74,6 +74,17 @@ void register_AI_handler( void (*callback)() );
/**
* @brief Register a VI callback
+ *
+ * This function will be called on any programmer VI interrupt, so possibly
+ * more than once per frame if the application uses line interrupts via
+ * the VI library. Normally, it will be called on vblank.
+
+ * @note Most applications should not use this function directly, but instead
+ * let the VI library manage interrupts, and use #vi_install_vblank_handler
+ * to install a custom vblank handler.
+ *
+ * @note VI interrupts are not generated until the VI is turned on. Use
+ * #vi_init or #display_init to turn the VI on.
*
* @param[in] callback
* Function to call when a VI interrupt occurs
@@ -396,6 +407,14 @@ void set_AI_interrupt( int active );
/**
* @brief Enable or disable the VI interrupt
*
+ * @note Most applications should not use this function directly, but instead
+ * let the VI library handle interrupts, and use #vi_install_vblank_handler
+ * to install a custom vblank handler.
+ *
+ * @note Even if enabled here in the interrupt controller, VI interrupts are not
+ * generated until the VI is turned on. Use #vi_init or #display_init to
+ * turn the VI on.
+ *
* The VI interrupt is generated when the VI begins displaying a specific line
* of the display output. The line number configured always refers to the
* final TV output, so it should be either in the range 0..524 (NTSC) or
@@ -616,7 +635,7 @@ void register_reset_handler( void (*callback)() )
* system. Therefore it is safe to nest disable/enable calls. After the least
* nested enable call, systemwide interrupts will be reenabled.
*/
-void enable_interrupts();
+void enable_interrupts(void);
/**
* @brief Disable interrupts systemwide
@@ -624,7 +643,7 @@ void enable_interrupts();
* @note If interrupts are already disabled on the system or interrupts have not
* been initialized, this function will not modify the system state.
*/
-void disable_interrupts();
+void disable_interrupts(void);
/**
@@ -634,7 +653,7 @@ void disable_interrupts();
* @retval INTERRUPTS_DISABLED if interrupts have been disabled.
* @retval INTERRUPTS_ENABLED if interrupts are currently enabled.
*/
-interrupt_state_t get_interrupts_state();
+interrupt_state_t get_interrupts_state(void);
#ifdef __cplusplus
}
diff --git a/include/joybus.h b/include/joybus.h
index 81e8a937b..932d80f19 100644
--- a/include/joybus.h
+++ b/include/joybus.h
@@ -7,10 +7,12 @@
#ifndef __LIBDRAGON_JOYBUS_H
#define __LIBDRAGON_JOYBUS_H
+
#include
#include
#include
#include
+#include "preview.h"
/**
* @defgroup joybus Joybus Subsystem
@@ -100,14 +102,10 @@ typedef uint16_t joybus_identifier_t;
* @name Joybus identifier values
* @{
*/
-/**
- * @brief Joybus identifier for an unknown or malfunctioning device.
- */
-#define JOYBUS_IDENTIFIER_UNKNOWN 0x0000
/**
* @brief Joybus identifier for a port with no device connected.
*/
-#define JOYBUS_IDENTIFIER_NONE 0xFFFF
+#define JOYBUS_IDENTIFIER_NONE 0x0000
/**
* @brief Joybus identifier for the Nintendo 64 voice recognition peripheral (NUS-020).
*
@@ -249,6 +247,35 @@ typedef uint16_t joybus_identifier_t;
#define JOYBUS_IDENTIFY_STATUS_EEPROM_BUSY 0x80
/** @} */
+/** @brief Joybus detection events enumeration */
+typedef enum {
+ JOYBUS_DETECT_DISCONNECTED = 0, ///< Device was just disconnected
+ JOYBUS_DETECT_POLLED = 1, ///< Device status was polled
+ JOYBUS_DETECT_CONNECTED = 2, ///< Device was just connected
+} joybus_detection_event_t;
+
+
+/**
+ * @brief Initialize the Joybus subsystem, and background detection support
+ * @preview
+ *
+ * This function starts the Joybus subsystem. It begins periodically monitoring
+ * all the controller ports (by default, once per second), to check
+ * whether new Joybus devices (normally, controllers) are plugged-in or removed.
+ *
+ * Libraries can register callbacks to be notified of detection events for
+ * any type of device via #joybus_register_detection_callback.
+ */
+LIBDRAGON_PREVIEW_API
+void joybus_init(void);
+
+/** @brief Deinitialize the joybus subsystem and detection support
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+void joybus_close(void);
+
+
/**
* @brief Callback function signature for #joybus_exec_async
*
@@ -404,10 +431,109 @@ inline void joybus_exec_cmd(
(void *)&cmd.recv \
)
+/**
+ * @brief Joybus detection callback function type.
+ *
+ * This function is called during the periodic poll of devices performed by
+ * the joybus module. It allows libraries to build upon the Joybus subsystem
+ * to provide higher-level functionality, and being notified of changes in the
+ * connected devices.
+ *
+ * At any point the callback is called, the device could have been just plugged,
+ * just unplugged, or just polled with no change. Since the hardware protocol
+ * allows the device to also relay a 8-bit status byte, this is also provided
+ * to the callback; the interpretation is device-specific.
+ *
+ * @param identifier The identifier of the Joybus device.
+ * @param event Whether the device was just plugged in, unplugged, or
+ * just polled with no change.
+ * @param port The Joybus port the device is connected to.
+ * @param device_status The status of the Joybus device as returned by it.
+ * @param ctx User-defined context pointer.
+ */
+typedef void (*joybus_detection_callback_t)(
+ joybus_identifier_t identifier, joybus_detection_event_t event,
+ int port, uint8_t device_status, void *ctx);
+
+/**
+ * @brief Register a callback to be notified of Joybus detection events.
+ * @preview
+ *
+ * This function registers a callback to be called whenever a Joybus device
+ * is plugged in or removed. The callback is called under interrupt, so it
+ * should either issue followup joybus commands asynchronously (via
+ * #joybus_exec_async), or arrange to handle the event later in a safer context.
+ *
+ * Multiple callbacks can be registered and they will all receive events for
+ * all devices on all ports. To unregister a callback, use
+ * #joybus_unregister_detection_callback.
+ *
+ * If you are building support for a specific joybus device, you should register
+ * your callback, filter events for the device identifier you support, and
+ * handle them accordingly.
+ *
+ * @note When calling this function, the callback will be immediately notified
+ * of all currently connected devices, with the #JOYBUS_DETECT_CONNECTED event.
+ *
+ * @param callback The callback function to register.
+ * @param ctx User-defined context pointer to pass to the callback.
+ *
+ * @see #joybus_unregister_detection_callback
+ */
+LIBDRAGON_PREVIEW_API
+void joybus_register_detection_callback(joybus_detection_callback_t callback, void *ctx);
+
+/**
+ * @brief Unregister a callback for Joybus detection events.
+ * @preview
+ *
+ * @param callback The callback function to unregister.
+ * @param ctx User-defined context pointer passed to the callback.
+ */
+LIBDRAGON_PREVIEW_API
+void joybus_unregister_detection_callback(joybus_detection_callback_t callback, void *ctx);
+
+/**
+ * @brief Immediately poll all Joybus ports for connected devices.
+ * @preview
+ *
+ * This function immediately polls all Joybus ports for connected devices,
+ * and calls any registered hotplug callbacks if any device was connected
+ * or disconnected.
+ *
+ * Normally, the Joybus subsystem automatically polls all ports once per
+ * second. This function can be used to force an immediate poll, for example
+ * after initializing the Joybus subsystem via #joybus_init.
+ */
+LIBDRAGON_PREVIEW_API
+void joybus_detect_now(void);
+
+/**
+ * @brief Get the Joybus identifier for a specific port.
+ *
+ * This function returns the last known identifier for a specific Joybus port.
+ * The identifier is updated automatically by the Joybus subsystem during
+ * periodic polling, or when #joybus_detect_now is called.
+ *
+ * @note The values returned by this function can change at any time as they
+ * react to interrupts.
+ *
+ * @param[in] port The Joybus port (0-4) to query.
+ * @param[out] status Optional pointer to a variable to store the device status.
+ * @return The Joybus identifier for the specified port.
+ */
+joybus_identifier_t joybus_get_identifier(int port, uint8_t *status);
+
+
#ifdef __cplusplus
}
#endif
/** @} */ /* joybus */
+///@cond
+// Deprecated variant of JOYBUS_IDENTIFIER_NONE
+#define JOYBUS_IDENTIFIER_UNKNOWN 0x0000
+///@endcond
+
#endif
diff --git a/include/joyframe.h b/include/joyframe.h
new file mode 100644
index 000000000..9fb129798
--- /dev/null
+++ b/include/joyframe.h
@@ -0,0 +1,450 @@
+/**
+ * @file joyframe.h
+ * @author Giovanni Bajo
+ * @brief Joybus frame construction macros
+ * @ingroup joybus
+ *
+ * This API allows for the construction of PIF frames in memory (also called
+ * joybus frames), that can be run via #joybus_exec / #joybus_exec_async.
+ *
+ * To create a frame, you can use #joyframe_write API macros. This is a simple
+ * example:
+ *
+ * @code{.c}
+ * joyframe_t pkt;
+ * joyframe_write_begin(pkt);
+ * joyframe_write_skip(pkt, 2); // Skip port 1-2
+ * uint8_t *send = joyframe_write(pkt, 2, 4, 0xAB); // Port 3: command 0xAB
+ * *send = 0x11; // First byte of send data
+ * joyframe_write_end(pkt);
+ *
+ * // Execute the frame (synchronously). Output will be written into
+ * // the same frame buffer.
+ * joybus_exec(pkt, pkt);
+ * @endcode
+ *
+ * Reading back the results of a frame can be done via #joyframe_read API macros.
+ * These macros allow to parse the frame and extract the results of each command.
+ * The macros are designed to parse arbitrary frames without previous knowledge
+ * of how they were constructed, and also handling all error conditions.
+ *
+ * @code{.c}
+ * joyframe_read_begin(pkt);
+ * joyframe_read_skip(pkt, 2); // Parse but ignore (skip) port 1-2
+ *
+ * joyframe_error_t err;
+ * uint8_t cmd; uint8_t *data;
+ *
+ * err = joyframe_read(pkt, &cmd, &data, NULL); // Parse port 3
+ * if (err < 0) {
+ * // Error handling: no device, timeout, etc.
+ * }
+ *
+ * // Check that the command is what we expected to read back.
+ * // If you just process the result of #joybus_exec that you constructed,
+ * // you are already sure of the contents of the frame, but in general you
+ * // could instead have a frame parsing code that handle different structures.
+ * assert(cmd == 0xAB);
+ *
+ * // Process the data received. The length of the data is known from
+ * // the command that was sent.
+ * debugf("Received %02X %02X %02X %02X\n", data[0], data[1], data[2], data[3]);
+ * @endcode
+ *
+ * It is important to handle error conditions as they can always happen, given
+ * that joybus devices are hot-pluggable. For instance, a device might have been
+ * disconnected by the player, so the command might return an error.
+ *
+ * Helpers for known commands
+ * --------------------------
+ * For known joybus commands (as publicly documented), there are helper macros
+ * that allow to write and read them more easily, as they take care of
+ * packing and unpacking the input and output data.
+ *
+ * For instance, the following code shows an example to perform a handshake to
+ * identify a device on the first port. This is normally not required as
+ * the joybus module, after #joybus_init, automatically performs background
+ * handshakes to identify connected devices, but it is shown here as an example.
+ *
+ * @code{.c}
+ * joyframe_t pkt;
+ * joyframe_write_begin(pkt);
+ * joyframe_write_N64_IDENTIFY(pkt);
+ * joyframe_write_end(pkt);
+ *
+ * // Execute the handshake (synchronously)
+ * joybus_exec(pkt, pkt);
+ *
+ * // Parse the result
+ * uint16_t device_id; uint8_t status;
+ * joyframe_read_begin(pkt);
+ * if (!joyframe_read_N64_IDENTIFY(pkt, &device_id, &status)) {
+ * // Error handling: no device, timeout, etc.
+ * }
+ * debugf("Device ID: %04X, status %02X\n", device_id, status);
+ * @endcode
+ *
+ * Libraries to handle custom joybus devices can expose functions with similar
+ * APIs to handle inserting their own commands into a joybus frame.
+ */
+#ifndef LIBDRAGON_JOYFRAME_H
+#define LIBDRAGON_JOYFRAME_H
+
+#include
+#include
+#include
+#include "debug.h"
+
+/**
+ * @brief Joybus frame escape codes
+ * \{
+ */
+#define JOYFRAME_ESC_SKIP 0x00 ///< Skip this port
+#define JOYFRAME_ESC_RESET 0xFD ///< Reset this port
+#define JOYFRAME_ESC_END 0xFE ///< End of frame
+#define JOYFRAME_ESC_NOP 0xFF ///< No operation, ignored
+/** \} */
+
+/**
+ * @brief Joybus TX/RX flag bits
+ * \{
+ */
+#define JOYFRAME_TX_SKIP 0x80 ///< Skip this port
+#define JOYFRAME_TX_RESET 0x40 ///< Reset this port
+#define JOYFRAME_RX_NO_DEVICE 0x80 ///< No device connected
+#define JOYFRAME_RX_TIMEOUT 0x40 ///< Error during transmission/reception
+/** \} */
+
+/** @brief A joybus frame */
+typedef uint8_t joyframe_t[64] __attribute__((aligned(8)));
+
+/**
+ * @brief Start writing a Joybus frame
+ *
+ * A joybus frame is a 64-byte buffer containing a series of commands to be sent
+ * to each port in sequence. It allows only for one command per port. If you
+ * don't have a command to send to the current port, you can write a "skip" command
+ * (#joyframe_write_skip) to skip it. The frame can be terminated early by writing
+ * an "End of frame" command (#joyframe_write_end),
+ *
+ * For instance, to write only a command to port 3 (1-based), you would do:
+ *
+ * @code{.c}
+ * joyframe_t pkt;
+ *
+ * joyframe_write_begin(pkt);
+ * joyframe_write_skip(pkt, 2); // Skip port 1-2
+ * joyframe_write_command(...); // Command for Port 3
+ * joyframe_write_end(pkt); // End of frame (auto skip ports 4-5)
+ * @endcode
+ *
+ * @hideinitializer
+ */
+#define joyframe_write_begin(pkt) ({ \
+ ((uint64_t *)pkt)[0] = 0; \
+ ((uint64_t *)pkt)[1] = 0; \
+ ((uint64_t *)pkt)[2] = 0; \
+ ((uint64_t *)pkt)[3] = 0; \
+ ((uint64_t *)pkt)[4] = 0; \
+ ((uint64_t *)pkt)[5] = 0; \
+ ((uint64_t *)pkt)[6] = 0; \
+ ((uint64_t *)pkt)[7] = 0; \
+})
+
+/**
+ * @brief Write a Joybus frame
+ *
+ * This macro writes a Joybus frame to the specified buffer. The command is
+ * the one that will be sent to the current port.
+ *
+ * @param pkt Joybus frame buffer
+ * @param send_len Length of the send data in bytes (including command byte)
+ * @param recv_len Length of the receive data in bytes
+ * @param cmd Command byte
+ */
+#define joyframe_write(pkt, send_len, recv_len, cmd) ({ \
+ int idx = pkt[63]; \
+ pkt[idx++] = send_len; \
+ pkt[idx++] = recv_len; \
+ pkt[idx++] = cmd; \
+ uint8_t *send_ptr = &pkt[idx]; \
+ pkt[63] = idx + send_len + recv_len - 1; \
+ assertf(pkt[63] < 64, "joybus frame overflow"); \
+ (send_len | recv_len) ? send_ptr : NULL; \
+})
+
+/**
+ * @brief Write a "Reset" command to the Joybus frame
+ *
+ * This macro writes a "Reset" command to the current port in the Joybus frame.
+ *
+ * The PIF will emit a special signal (keep the line low for 1 ms), which is
+ * meant to reset the connected device. Not all devices support this command,
+ * and the actual behavior on joybus devices is not well documented.
+ *
+ * @hideinitializer
+ */
+#define joyframe_write_reset(pkt) ({ \
+ int idx = pkt[63]; \
+ pkt[idx++] = JOYFRAME_ESC_RESET; \
+ pkt[63] = idx; \
+})
+
+/**
+ * @brief Write a "Skip" command to the Joybus frame
+ *
+ * This macro should be used whenever you don't have a command to send
+ * to the current port. It tells the PIF to skip this port and move
+ * to the next one.
+ *
+ * @hideinitializer
+ */
+#define joyframe_write_skip(pkt, n) ({ \
+ int __n = (n); \
+ int idx = pkt[63]; \
+ for (int i = 0; i < __n; i++) { \
+ pkt[idx++] = JOYFRAME_ESC_SKIP; \
+ } \
+ pkt[63] = idx; \
+})
+
+/**
+ * @brief Write an "End of frame" command to the Joybus frame
+ *
+ * This macro marks the frame as ended. The PIF will skip any remaining
+ * ports after this command.
+ *
+ * Make sure to always call this macro after writing your commands.
+ *
+ * @hideinitializer
+ */
+#define joyframe_write_end(pkt) ({ \
+ int idx = pkt[63]; \
+ pkt[idx++] = JOYFRAME_ESC_END; \
+ assertf(idx < 64, "joybus frame overflow"); \
+ pkt[63] = 0x01; \
+})
+
+
+/**
+ * @brief Joybus frame status codes for #joyframe_read
+ *
+ * These codes are returned by #joyframe_read to indicate the parsing status
+ * of the command read from the frame.
+ *
+ * The codes are divided into two categories:
+ * * Positive values (including zero) indicate success, possibly with special
+ * escape codes being parsed. For instance, if the port was skipped or reset,
+ * the function returns a positive value with value #JOYFRAME_SKIPPED or
+ * #JOYFRAME_RESET respectively.
+ * * Negative values indicate errors, such as no device connected, timeout,
+ * or that the frame has been fully parsed.
+ */
+typedef enum {
+ JOYFRAME_RESET = 2, ///< Port was reset by the frame
+ JOYFRAME_SKIPPED = 1, ///< Port was skipped in the frame
+
+ JOYFRAME_ERR_NONE = 0,
+ JOYFRAME_ERR_NO_DEVICE = -1, ///< No device connected to this port
+ JOYFRAME_ERR_TIMEOUT = -2, ///< Device did not respond in time (wrong command?)
+ JOYFRAME_ERR_FINISHED = -4, ///< The frame does not contain a command for this port
+} joyframe_err_t;
+
+
+/**
+ * @brief Initialize reading a Joybus frame
+ *
+ * @hideinitializer
+ */
+#define joyframe_read_begin(pkt) ({ \
+ pkt[63] = 0; \
+})
+
+/**
+ * @brief Skip a number of ports in the Joybus frame
+ *
+ * If you are not interested in reading the commands for a number of ports,
+ * you can use this macro to skip them.
+ */
+#define joyframe_read_skip(pkt, n) ({ \
+ int __n = (n); \
+ for (int i = 0; i < __n; i++) { \
+ (void)joyframe_read(pkt, NULL, NULL); \
+ } \
+})
+
+/**
+ * @brief Read the next command from the frame
+ *
+ * This function reads the command for next port from the Joybus frame.
+ * It is designed to parse arbitrary frames even without previous knowledge of
+ * the contents, and also including malformed ones.
+ *
+ * Each call to this function returns the command for the next port in sequence,
+ * including the command ID, the data received from the device and the length
+ * of the data.
+ *
+ * It is mandatory to also check the return value of this function, as it can
+ * indicate errors such as no device connected, timeout, etc. Handling errors
+ * is important, as joybus devices are hot-pluggable and errors can always happen.
+ *
+ * If the frame contained the special "skip" or "reset" commands for a certain
+ * port (as written via #joyframe_write_skip or #joyframe_write_reset), the
+ * function return a non-zero positive value, and the variables pointed by
+ * `cmd`, `data` and `data_len` are not modified.
+ *
+ * @param[in] pkt Joybus frame buffer
+ * @param[out] cmd Command byte read from the frame (optional, can be NULL)
+ * @param[out] data Pointer to the data buffer read from the frame (optional, can be NULL)
+ * @param[out] data_len Length of the data buffer read from the frame (optional, can be NULL)
+ * @return Negative values for errors, 0 or positive values for success
+ */
+__attribute__ ((warn_unused_result))
+joyframe_err_t joyframe_read(joyframe_t pkt, uint8_t *cmd, uint8_t **data, int *data_len);
+
+
+/**
+ * @name Helpers to write/read common Joybus commands
+ *
+ * These helpers show how to implement wrappers for #joyframe_write and #joyframe_read
+ * to help library code and user code to build and parse Joybus frames.
+ *
+ * The only two commands that we treat as valid for all Joybus devices (and are
+ * in fact implemented and emitted by the joybus library itself) are IDENTIFY
+ * and RESET.
+ *
+ * All other commands handled here as just meant as examples, and will only work
+ * when issued to the correct type of device. For instance, the N64 controller
+ * read command will only work when issued to an N64 controller or to a device
+ * that emulates it.
+ *
+ * \{
+ */
+
+/** @brief Write the RESET command to a joyframe */
+inline void joyframe_write_RESET(joyframe_t pkt)
+{
+ joyframe_write(pkt, 1, 3, 0xFF);
+}
+
+/**
+ * @brief Read the result of the RESET command from a joyframe
+ *
+ * @param pkt Joybus frame buffer
+ * @param[out] device_id Pointer to store the device ID (optional, can be NULL)
+ * @param[out] device_status Pointer to store the device status (optional, can be NULL)
+ * @return true if the command was successfully read, false otherwise
+ */
+__attribute__ ((warn_unused_result))
+inline bool joyframe_read_RESET(joyframe_t pkt, uint16_t *device_id, uint8_t *device_status)
+{
+ uint8_t cmd = 0; uint8_t *data = NULL; int data_len = 0;
+ if (joyframe_read(pkt, &cmd, &data, &data_len) != JOYFRAME_ERR_NONE) return false;
+ if (cmd != 0xFF || data_len != 3) return false;
+ if (device_id) *device_id = (data[0] << 8) | data[1];
+ if (device_status) *device_status = data[2];
+ return true;
+}
+
+/** @brief Write the IDENTIFY command to a joyframe */
+inline void joyframe_write_IDENTIFY(joyframe_t pkt)
+{
+ joyframe_write(pkt, 1, 3, 0x00);
+}
+
+/** @brief Read the result of an IDENTIFY command from a joyframe
+ *
+ * Identifiers are defined in joybus.h as JOYBUS_IDENTIFIER_* macros.
+ * Device status is device-specific.
+ *
+ * @param pkt Joybus frame buffer
+ * @param[out] device_id Pointer to store the device ID (optional, can be NULL)
+ * @param[out] device_status Pointer to store the device status (optional, can be NULL)
+ * @return true if the command was successfully read, false otherwise
+ */
+__attribute__ ((warn_unused_result))
+inline bool joyframe_read_IDENTIFY(joyframe_t pkt, uint16_t *device_id, uint8_t *device_status)
+{
+ uint8_t cmd = 0; uint8_t *data = NULL; int data_len = 0;
+ if (joyframe_read(pkt, &cmd, &data, &data_len) != JOYFRAME_ERR_NONE) return false;
+ if (cmd != 0x00 || data_len != 3) return false;
+ if (device_id) *device_id = (data[0] << 8) | data[1];
+ if (device_status) *device_status = data[2];
+ return true;
+}
+
+/** @brief Write the N64 controller read command to a joyframe */
+inline void joyframe_write_N64_CONTROLLER_READ(joyframe_t pkt)
+{
+ joyframe_write(pkt, 1, 4, 0x01);
+}
+
+/**
+ * @brief Write the N64 accessory read command to a joyframe
+ *
+ * @param pkt Joybus frame buffer
+ * @param address Accessory address to read from (16-bit, must include checksum)
+ */
+inline void joyframe_write_N64_ACCESSORY_READ(joyframe_t pkt, uint16_t address)
+{
+ uint8_t *send_ptr = joyframe_write(pkt, 3, 33, 0x02);
+ *send_ptr++ = address >> 8;
+ *send_ptr++ = address & 0xFF;
+}
+
+
+/**
+ * @brief Read the result of a N64 accessory read command from a joyframe
+ *
+ * @param pkt Joybus frame buffer
+ * @param[out] data Pointer to store the data read (32 bytes)
+ * @param[out] checksum Pointer to store the checksum read (1 byte)
+ * @return true if the command was successfully read, false otherwise
+ */
+__attribute__ ((warn_unused_result))
+inline bool joyframe_read_N64_ACCESSORY_READ(joyframe_t pkt, uint8_t **data, uint8_t *checksum)
+{
+ uint8_t cmd = 0; int data_len = 0;
+ if (joyframe_read(pkt, &cmd, data, &data_len) != JOYFRAME_ERR_NONE) return false;
+ if (cmd != 0x02 || data_len != 33) return false;
+ *checksum = (*data)[32];
+ return true;
+}
+
+/**
+ * @brief Write the N64 accessory write command to a joyframe
+ *
+ * @param pkt Joybus frame buffer
+ * @param address Accessory address to write to (16-bit, must include checksum)
+ * @param data Pointer to the data to write (32 bytes)
+ */
+inline void joyframe_write_N64_ACCESSORY_WRITE(joyframe_t pkt, uint16_t address, const uint8_t data[32])
+{
+ uint8_t *send_ptr = joyframe_write(pkt, 35, 1, 0x03);
+ *send_ptr++ = address >> 8;
+ *send_ptr++ = address & 0xFF;
+ memcpy(send_ptr, data, 32);
+}
+
+/**
+ * @brief Read the result of a N64 accessory write command from a joyframe
+ *
+ * @param pkt Joybus frame buffer
+ * @param[out] checksum Pointer to store the checksum read (1 byte)
+ * @return true if the command was successfully read, false otherwise
+ */
+__attribute__ ((warn_unused_result))
+inline bool joyframe_read_N64_ACCESSORY_WRITE(joyframe_t pkt, uint8_t *checksum)
+{
+ uint8_t cmd = 0; uint8_t *data = NULL; int data_len = 0;
+ if (joyframe_read(pkt, &cmd, &data, &data_len) != JOYFRAME_ERR_NONE) return false;
+ if (cmd != 0x03 || data_len != 1) return false;
+ *checksum = data[0];
+ return true;
+}
+
+/**
+ * \}
+ */
+
+#endif // LIBDRAGON_JOYFRAME_H
diff --git a/include/joypad.h b/include/joypad.h
index a6131fc3b..6d8001703 100644
--- a/include/joypad.h
+++ b/include/joypad.h
@@ -44,10 +44,6 @@
* to Nintendo 64 controllers. Call #joypad_get_accessory_type to determine
* which accessory was detected.
*
- * For advanced use-cases, a developer can determine exactly which type of
- * input device is connected by calling #joypad_get_identifier, which will
- * return the 16-bit device identifier value from the Joybus "Info" response.
- *
* To read digital button state for a Joypad device:
* * #joypad_get_buttons
* * #joypad_get_buttons_pressed
@@ -485,15 +481,6 @@ void joypad_poll(void);
*/
bool joypad_is_connected(joypad_port_t port);
-/**
- * @brief Get the Joybus device identifier for a Joypad port.
- *
- * @param port Joypad port number (#joypad_port_t)
- *
- * @return Joybus device identifier (#joybus_identifier_t)
- */
-joybus_identifier_t joypad_get_identifier(joypad_port_t port);
-
/**
* @brief Get the Joypad style for a Joypad port.
*
@@ -514,6 +501,9 @@ joypad_accessory_type_t joypad_get_accessory_type(joypad_port_t port);
/**
* @brief Is rumble supported for a Joypad port?
+ *
+ * Check if the Joypad port supports rumble motors (on either the N64 or
+ * GameCube controller).
*
* @param port Joypad port number (#joypad_port_t)
*
@@ -523,6 +513,9 @@ bool joypad_get_rumble_supported(joypad_port_t port);
/**
* @brief Is rumble active for a Joypad port?
+ *
+ * Check if rumble is currently active for a Joypad port, for either the N64 or
+ * GameCube controller.
*
* @param port Joypad port number (#joypad_port_t)
*
@@ -532,6 +525,9 @@ bool joypad_get_rumble_active(joypad_port_t port);
/**
* @brief Activate or deactivate rumble on a Joypad port.
+ *
+ * Activate or deactivate rumble on a Joypad port, for either the N64 or
+ * GameCube controller.
*
* @param port Joypad port number (#joypad_port_t)
* @param active Whether rumble should be active
@@ -631,6 +627,15 @@ int joypad_get_axis_released(joypad_port_t port, joypad_axis_t axis);
*/
int joypad_get_axis_held(joypad_port_t port, joypad_axis_t axis);
+
+///@cond
+__attribute__((deprecated("Use joybus_get_identifier instead")))
+static inline joybus_identifier_t joypad_get_identifier(joypad_port_t port) {
+ return joybus_get_identifier(port, NULL);
+}
+///@endcond
+
+
#ifdef __cplusplus
}
#endif
diff --git a/include/kernel.h b/include/kernel.h
new file mode 100644
index 000000000..88a5c3799
--- /dev/null
+++ b/include/kernel.h
@@ -0,0 +1,662 @@
+/**
+ * @file kernel.h
+ * @author Giovanni Bajo
+ * @brief Multi-threading kernel
+ * @ingroup kernel
+ */
+#ifndef KERNEL_H
+#define KERNEL_H
+
+#include
+#include
+#include
+
+#include "preview.h"
+#include "exception.h"
+#include "n64sys.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @defgroup kernel Multi-threading kernel
+ * @ingroup libdragon
+ * @brief A multi-threading kernel that can be used by libdragon applications.
+ *
+ * This module implements a hybrid cooperative/preemptive multi-threaded
+ * kernel for parallel execution of code.
+ *
+ * The scheduler uses a very simple logic:
+ *
+ * * A thread is "ready" whenever is able to run, that is, it is not waiting
+ * for some event or otherwise sleeping. A ready thread can technically be
+ * scheduled at any time, whenever the scheduler decides so.
+ * * When a thread switch happens, the scheduler selects the ready thread which
+ * has the highest priority.
+ * * If there are multiple threads with the same priority (higher than any
+ * other ready thread), the scheduler will round-robin among them.
+ *
+ * A thread switch can happen in a few specific situations:
+ *
+ * * The active thread explicitly starts waiting for some event, or sleeps
+ * for a definite amount of time. In general, whenever a kthread_* function
+ * is invoked, it might cause a context switch.
+ * * An interrupt occurs, which triggers an event, which wakes up some thread
+ * whose priority is higher than the currently running thread.
+ *
+ * Given the above rules, we can say that the kernel is "hard real-time": any
+ * thread with high priority that is ready (not blocked) will always have
+ * priority over lower priority threads. This is required to allow threads to
+ * implement operations that require low latency, for instance preparing audio
+ * when the AI interrupt fires.
+ *
+ * Also, the kernel is not fully preemptive; in particular, there is no timer
+ * interrupt that switches among ready threads at a fixed interval. This
+ * is not deemed necessary as applications will usually have a low number
+ * threads that are mostly blocked waiting for specific events like
+ * hardware interrupts or background activities like RSP ucode.
+ *
+ * @{
+ */
+
+/**
+ * @brief a kernel thread for parallel execution
+ *
+ * This structure represents a thread that can be scheduled
+ * for parallel execution. Create a thread with #kthread_new,
+ * allocating the required memory for the stack.
+ *
+ * Normally, there's no need to explicitly manage the lifetime
+ * or state of a thread. Once the thread is created, it is
+ * immediately started, and the kernel will scheduled it when
+ * required. If the thread exits (by simply returning by its
+ * entry point function), it will be cleaned up and the memory
+ * released.
+ *
+ * #kthread_new returns a pointer to the thread (a pointer
+ * to kthread_t) that can be used to externally manage the
+ * thread like changing its priority or killing it.
+ */
+typedef struct kthread_s kthread_t;
+
+/**
+ * @brief A mutex for synchronization
+ *
+ * A mutex is a synchronization primitive that can be used to protect
+ * shared resources from concurrent access. A mutex can be locked by
+ * only one thread at a time; if another thread tries to lock a mutex
+ * that is already locked, it will block until the mutex is unlocked.
+ *
+ * The mutex can be created with the KMUTEX_RECURSIVE flag, so that
+ * the same thread can lock the mutex multiple times without blocking.
+ * In this case, the mutex must be unlocked the same number of times
+ * it was locked.
+ *
+ * To create a mutex, you can either zero-initialize it or use #kmutex_init.
+ *
+ * @note The contents of this structure are subject to change and should be
+ * considered internal. Do not access or modify any field directly. The structure
+ * is exposed only to allow creation in a static context (that is, without malloc).
+ *
+ * @see #kmutex_init
+ * @see #kmutex_lock
+ * @see #kmutex_unlock
+ * @see #kmutex_destroy
+ */
+typedef struct kmutex_s {
+ ///@cond
+ int8_t original_pri : 8; // Original priority of the owner thread
+ uint8_t flags : 1; // Flags for the mutex
+ phys_addr_t owner : 23; // Owner thread
+ uint8_t counter : 8; // Recursive lock counter
+ phys_addr_t waiting : 24; // List of waiting threads
+ ///@endcond
+} kmutex_t;
+
+_Static_assert(sizeof(kmutex_t) == 8, "kmutex_t size mismatch");
+
+#define KMUTEX_STANDARD 0 ///< Standard mutex
+#define KMUTEX_RECURSIVE (1<<0) ///< Recursive mutex
+
+
+/**
+ * @brief A condition variable for synchronization
+ *
+ * A condition variable is a synchronization primitive that allows
+ * threads to wait for a specific condition to happen. A condition
+ * variable is always associated with a mutex, and the mutex must be
+ * locked before calling #kcond_wait or #kcond_signal.
+ *
+ * @note The contents of this structure are subject to change and should be
+ * considered internal. Do not access or modify any field directly. The structure
+ * is exposed only to allow creation in a static context (that is, without malloc).
+ *
+ * @see #kcond_init
+ * @see #kcond_wait
+ * @see #kcond_signal
+ * @see #kcond_broadcast
+ *
+ */
+typedef struct kcond_s {
+ kthread_t *waiting; ///< List of waiting threads
+} kcond_t;
+
+/**
+ * @brief Initialize the multi-threading kernel
+ * @preview
+ *
+ * The current execution context becomes the main thread of the
+ * program, with priority set to 0 (you can change priority
+ * to the main thread as well using #kthread_set_pri).
+ *
+ * The main thread uses the original stack allocated for the
+ * whole process, so it is technically unbounded (or limited by
+ * heap size). The priority of the main thread is 0.
+ *
+ * @return a pointer to the main thread.
+ *
+ */
+LIBDRAGON_PREVIEW_API
+kthread_t* kernel_init(void);
+
+/**
+ * @brief Shutdown the multi-threading kernel
+ * @preview
+ *
+ * This function is mostly useful for testing purposes.
+ * Since the kernel does not keep track of all created threads,
+ * this function should be called only when all created threads
+ * are exited or have been killed.
+ */
+LIBDRAGON_PREVIEW_API
+void kernel_close(void);
+
+/**
+ * @brief Create a new thread
+ * @preview
+ *
+ * Create a new thread, with a specified stack size and priority.
+ * The thread is immediately made ready after creation, so if it has
+ * a priority higher than or equal to the current thread, it will be
+ * scheduled immediately, before thread_new returns.
+ *
+ * The thread will begin execution from the specified entry point
+ * function. If the function ever returns, the thread is automatically
+ * killed.
+ *
+ * @param[in] name
+ * Name of the thread (for debugging purposes)
+ * @param[in] stack_size
+ * Size of the stack in bytes. Minimum suggested size
+ * is 2048.
+ * @param[in] pri
+ * Priority of the thread (-128 .. 127). Higher number means higher
+ * priority. Main thread is conventionally set at 0, so you can use
+ * positive numbers for "high priority" tasks that should interrupt
+ * the main thread (eg: audio), and negative numbers for stuff that
+ * needs to happen "in background" while the main thread is idle.
+ * @param[in] user_entry
+ * Entry point of the thread. This is the function that will
+ * be called when the thread begins execution. If this function
+ * ever returns, the thread is automatically killed, and the return
+ * value will be its exit code.
+ * @param[in] user_data
+ * Argument that will be passed to the entry point of the thread.
+ *
+ * @return A pointer to the new thread. It is not necessary to store
+ * this reference if not required; the thread will clean up
+ * after itself when it exits.
+ */
+LIBDRAGON_PREVIEW_API
+kthread_t* kthread_new(const char *name, int stack_size, int8_t pri,
+ int (*user_entry)(void*), void *user_data);
+
+
+/**
+ * @brief Return a reference to the current running thread
+ * @preview
+ *
+ * @return Reference to the current thread
+ */
+LIBDRAGON_PREVIEW_API
+kthread_t* kthread_current(void);
+
+
+/**
+ * @brief Yield execution of the current thread and run the scheduler.
+ * @preview
+ *
+ * This function allows the current thread to cooperatively yield
+ * its execution to allow other threads to run.
+ *
+ * The scheduler will switch to the highest priority thread that is
+ * currently ready to run. If no ready thread has a priority higher than
+ * the current thread, the scheduler will switch to a different thread
+ * of the same priority of the current one (the scheduler will guarantee
+ * a correct round-robin scheduling among threads of the same priority).
+ * If no ready thread has a priority higher or equal than the current
+ * one, the scheduler will reschedule the current thread, that will
+ * continue execution.
+ *
+ * @note The scheduler is semi-preemptive. Any interrupt could cause
+ * a thread switch to happen if the interrupt itself makes a thread
+ * become ready. If you need a block of code to be executed without
+ * any context switch, make sure to disable interrupts.
+ */
+LIBDRAGON_PREVIEW_API
+void kthread_yield(void);
+
+/**
+ * @brief Suspend the specified thread
+ * @preview
+ *
+ * Suspends the specified thread, so that it will not be scheduled anymore.
+ * The thread will be put in a "suspended" state, and will not be scheduled
+ * until it is resumed.
+ *
+ * @param th Reference to the thread to suspend
+ */
+LIBDRAGON_PREVIEW_API
+void kthread_suspend(kthread_t *th);
+
+/**
+ * @brief Resume a thread that was previously suspended
+ * @preview
+ *
+ * @param th Reference to the thread to resume
+ */
+LIBDRAGON_PREVIEW_API
+void kthread_resume(kthread_t *th);
+
+/**
+ * @brief Return the backtrace of the specified thread
+ * @preview
+ *
+ * This function has the same semantic of #backtrace, but it returns
+ * the backtrace of the specified thread instead of the current one.
+ *
+ * When called with a NULL argument, this function returns the backtrace
+ * of the current thread, and thus behaves identical to #backtrace.
+ *
+ * @param[in] th Reference to the thread to backtrace (NULL = current thread)
+ * @param[out] buffer Buffer where to store the backtrace
+ * @param[in] size Size of the buffer
+ * @return Number of entries in the backtrace
+ */
+LIBDRAGON_PREVIEW_API
+int kthread_backtrace(kthread_t *th, void *buffer, int size);
+
+/**
+ * @brief Sleep for the specified interval, allowing execution of other threads.
+ * @preview
+ *
+ * This function will put the current thread to sleep for a specified
+ * time interval, allowing other threads to run.
+ *
+ * The sleeping interval is expressed in hardware ticks. See #TICKS_READ
+ * for an explanation of hardware ticks, and #TICKS_PER_SECOND and #TICKS_FROM_MS
+ * for constants and macros that simplify conversion to standard time units.
+ *
+ * Notice that #wait_ticks and #wait_ms are not threading aware, so they will
+ * just spin-wait while keeping the current thread ready. Other higher-priority
+ * threads might still be scheduled if they are awaken by an interrupt, but in
+ * general thread_sleep is a better primitive if you want the current thread
+ * to yield.
+ *
+ * @note This function requires the timer module, so #timer_init must have
+ * been called.
+ *
+ * @param[in] ticks
+ * The number of hardware ticks to sleep.
+ */
+LIBDRAGON_PREVIEW_API
+void kthread_sleep(int64_t ticks);
+
+/**
+ * @brief Change priority of a thread.
+ * @preview
+ *
+ * Change priority of the specified thread. If the argument is NULL,
+ * this function changes priority to the current thread.
+ *
+ * The change of priority is immediately effective. It may cause
+ * a context switch if the changed thread is ready and its priority
+ * is changed in a way to start/stop it relative to the other ready
+ * threads.
+ *
+ * @param[in] th
+ * Reference to the thread (NULL = current thread)
+ * @param[in] pri
+ * New priority of the thread (0-127). Higher number means
+ * higher priority.
+ *
+ */
+LIBDRAGON_PREVIEW_API
+void kthread_set_pri(kthread_t *th, int8_t pri);
+
+
+/**
+ * @brief Kill a thread, aborting its execution.
+ * @preview
+ *
+ * The specified thread is aborted, and its memory freed (including
+ * its stack). The execution will be aborted and the memory released
+ * to the heap for further use.
+ *
+ * This function can executed for any thread, including the
+ * current one, in which case it becomes equivalent to #kthread_exit.
+ *
+ * @param[in] th Reference to the thread to kill. Since NULL refers to the
+ * current thread, calling this function passing NULL is
+ * equivalent to #kthread_exit.
+ * @param[in] res Result code of the thread that will be killed
+ *
+ * @see #kthread_exit
+ */
+LIBDRAGON_PREVIEW_API
+void kthread_kill(kthread_t *th, int res);
+
+/**
+ * @brief Lock the current thread (disable preemption)
+ *
+ * This function makes the current thread "locked": the kernel will not switch
+ * away from it, and keep it running until #kthread_unlock is called.
+ *
+ * This function is extremely fast, and lighter than a mutex. On the other
+ * hand, it does not allow for granular locking of resources since preemption
+ * is fully disabled.
+ *
+ * The intended use case to create *small* critical sections where you want
+ * only the current thread to manipulate some shared data structure.
+ *
+ * You can nest calls to kthread_lock() multiple times, as long as you then
+ * call #kthread_unlock the same number of times.
+ */
+// TODO: implement a proper kernel locking that doesn't disable interrupts, to
+// avoid adding interrupt latency
+#define kthread_lock() disable_interrupts()
+
+/**
+ * @brief Unlock the current thread (reenable preemption)
+ */
+#define kthread_unlock() enable_interrupts()
+
+
+/**
+ * @brief Exit from a thread, providing a result value
+ * @preview
+ *
+ * This function allows to abort the execution of the current thread, optionally
+ * providing a result value. If the thread is not detached, the result value
+ * can be read by the joiner thread via #kthread_join. If the thread is detached,
+ * the return value will be ignored.
+ *
+ * Returning from a thread's entry point is equivalent to calling kthread_exit.
+ *
+ * @param res Result value
+ */
+__attribute__((noreturn))
+LIBDRAGON_PREVIEW_API
+void kthread_exit(int res);
+
+/**
+ * @brief Detach a thread, so that it can terminated without any join
+ * @preview
+ *
+ * By defaults, kernel threads are "attached" to the main thread; this
+ * means that to fully terminate, there should be a thread calling kthread_join
+ * on then. Joining a thread can happen before or after it finishes execution,
+ * but it has to happen for the thread to be fully cleaned up.
+ *
+ * If a thread is detached, it can be terminated without any join. The thread
+ * will be fully cleaned up when it finishes execution.
+ *
+ * NOTE: Pay attention to race conditions when detaching threads that you have
+ * just created. It might be advisable to let a thread detaches itself, so that
+ * you don't risk to detach a thread that is already finished.
+ *
+ * @param[in] th Reference to the thread to detach (or NULL to
+ * detach the current thread)
+ */
+LIBDRAGON_PREVIEW_API
+void kthread_detach(kthread_t *th);
+
+
+/**
+ * @brief Wait for a thread to finish.
+ * @preview
+ *
+ * This function blocks the current thread until the specified
+ * thread finishes its execution. The CPU is yielded so that other
+ * threads will be scheduled.
+ *
+ * @param[in] th Reference to the thread to wait for
+ * @result Result code of the thread that was joined
+ */
+LIBDRAGON_PREVIEW_API
+int kthread_join(kthread_t *th);
+
+/**
+ * @brief Check if a thread is finished without blocking
+ * @preview
+ *
+ * This function is similar to #kthread_join, but it does not block if
+ * the thread is not finished yet. If the thread is finished, it returns
+ * true and stores the result code in the res argument. If the thread is
+ * still running, it returns false.
+ *
+ * Notice that the thread *is* joined if the function returns true; after
+ * that, the thread is fully cleaned up and the memory released, so the thread
+ * pointer becomes invalid.
+ *
+ * @param[in] th Reference to the thread to wait for
+ * @param[out] res Pointer to the result code of the thread
+ * @return true if the thread is finished and successfully joined
+ * @return false if the thread is still running
+ */
+LIBDRAGON_PREVIEW_API
+bool kthread_try_join(kthread_t *th, int *res);
+
+/**
+ * @brief Return the name of the specified thread.
+ * @preview
+ *
+ * @param[in] th
+ * Reference to thread (NULL = current thread)
+ */
+LIBDRAGON_PREVIEW_API
+const char* kthread_name(kthread_t *th);
+
+/**
+ * @brief Inititalize a new mutex.
+ * @preview
+ *
+ * A mutex is a synchronization primitive that can be used to protect
+ * shared resources from concurrent access. A mutex can be locked by
+ * only one thread at a time; if another thread tries to lock a mutex
+ * that is already locked, it will block until the mutex is unlocked.
+ *
+ * The mutex can be created with the KMUTEX_RECURSIVE flag, so that
+ * the same thread can lock the mutex multiple times without blocking.
+ * In this case, the mutex must be unlocked the same number of times
+ * it was locked.
+ *
+ * Notice that it's not mandatory to initialize a mutex with this functions,
+ * as zeroing the memory is enough to create a valid, non-recursive mutex.
+ *
+ * @note Mutexes are not recursive by default.
+ *
+ * @param mutex Pointer to the mutex to initialize
+ * @param flags Flags for the mutex. Use #KMUTEX_STANDARD for a standard
+ * mutex, or #KMUTEX_RECURSIVE for a recursive mutex.
+ *
+ * @see #kmutex_destroy
+ */
+LIBDRAGON_PREVIEW_API
+void kmutex_init(kmutex_t *mutex, uint8_t flags);
+
+/**
+ * @brief Destroy a mutex.
+ * @preview
+ *
+ * @param mtx Pointer to the mutex to free
+ */
+LIBDRAGON_PREVIEW_API
+void kmutex_destroy(kmutex_t *mtx);
+
+/**
+ * @brief Acquire a lock to the mutex
+ * @preview
+ *
+ * This function tries to acquire a lock to the mutex. If the mutex
+ * is already locked, the thread will block until the mutex is unlocked.
+ *
+ * A thread can lock a mutex multiple times only if the mutex was created
+ * with the KMUTEX_RECURSIVE flag. In this case, the mutex must be unlocked
+ * the same number of times it was locked.
+ *
+ * For a non-blocking or timed version, see #kmutex_try_lock.
+ *
+ * @param mtx Pointer to the mutex
+ *
+ * @see #kmutex_unlock
+ * @see #kmutex_try_lock
+ */
+LIBDRAGON_PREVIEW_API
+void kmutex_lock(kmutex_t *mtx);
+
+/**
+ * @brief Release a lock to the mutex
+ * @preview
+ *
+ * @param mtx Pointer to the mutex
+ */
+LIBDRAGON_PREVIEW_API
+void kmutex_unlock(kmutex_t *mtx);
+
+/**
+ * @brief Try to acquire a lock to the mutex for a specified amount of time
+ * @preview
+ *
+ * This function tries to acquire a lock to the mutex. If the mutex
+ * is already locked, the thread will block until the mutex is unlocked
+ * but only for the specified amount of @p ticks. If the mutex is not
+ * unlocked in time, the function will return false.
+ *
+ * As a special case, if @p ticks is 0, the function will never block
+ * and will return false immediately if the mutex is already locked.
+ *
+ * @param mtx Pointer to the mutex
+ * @param ticks Number of hardware ticks to wait for the mutex. See
+ * #TICKS_FROM_MS or #TICKS_FROM_US for conversion macros.
+ * @return true If the mutex was successfully locked
+ * @return false If the mutex was not locked in time
+ */
+LIBDRAGON_PREVIEW_API
+bool kmutex_try_lock(kmutex_t *mtx, uint32_t ticks);
+
+/**
+ * @brief Initialize a condition variable
+ * @preview
+ *
+ * A condition variable is a synchronization primitive that allows
+ * threads to wait for a specific condition to happen. A condition
+ * variable is always associated with a mutex, and the mutex must be
+ * locked before calling #kcond_wait or #kcond_signal.
+ *
+ * @param[in] cond Pointer to the condition variable to initialize
+ *
+ * @see #kcond_destroy
+ */
+LIBDRAGON_PREVIEW_API
+void kcond_init(kcond_t *cond);
+
+/**
+ * @brief Destroy a condition variable
+ * @preview
+ *
+ * @param cond Pointer to the condition variable
+ */
+LIBDRAGON_PREVIEW_API
+void kcond_destroy(kcond_t *cond);
+
+/**
+ * @brief Wait for a condition to happen
+ * @preview
+ *
+ * This function will block the current thread until the condition
+ * variable is signaled. The mutex must be locked before calling
+ * this function. It will be released while the thread is waiting
+ * and re-acquired when the thread is woken up.
+ *
+ * @param cond Pointer to the condition variable
+ * @param mtx Pointer to the mutex
+ *
+ * @see #kcond_signal
+ * @see #kcond_broadcast
+ */
+LIBDRAGON_PREVIEW_API
+void kcond_wait(kcond_t *cond, kmutex_t *mtx);
+
+
+/**
+ * @brief Wait for a condition to happen for a specified amount of time
+ * @preview
+ *
+ * This function will block the current thread until the condition
+ * variable is signaled, or until the specified amount of time has
+ * passed. The mutex must be locked before calling this function.
+ * It will be released while the thread is waiting and re-acquired
+ * when the thread is woken up.
+ *
+ * @param cond Pointer to the condition variable
+ * @param mtx Pointer to the mutex
+ * @param ticks Number of hardware ticks to wait for the condition.
+ *
+ * @return true If the condition was signaled
+ * @return false If the condition was not signaled in time
+ */
+LIBDRAGON_PREVIEW_API
+bool kcond_wait_timeout(kcond_t *cond, kmutex_t *mtx, uint32_t ticks);
+
+
+/**
+ * @brief Signal a condition variable
+ * @preview
+ *
+ * This function will wake up one thread that is waiting on the
+ * condition variable. If no thread is waiting, the signal is
+ * ignored.
+ *
+ * @param cond Pointer to the condition variable
+ *
+ * @see #kcond_wait
+ * @see #kcond_broadcast
+ */
+LIBDRAGON_PREVIEW_API
+void kcond_signal(kcond_t *cond);
+
+/**
+ * @brief Broadcast a condition variable
+ * @preview
+ *
+ * This function will wake up all threads that are waiting on the
+ * condition variable. If no thread is waiting, the broadcast is
+ * ignored.
+ *
+ * @param cond Pointer to the condition variable
+ *
+ * @see #kcond_wait
+ * @see #kcond_signal
+ */
+LIBDRAGON_PREVIEW_API
+void kcond_broadcast(kcond_t *cond);
+
+/** @} */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* KERNEL_H */
diff --git a/include/kirq.h b/include/kirq.h
new file mode 100644
index 000000000..dd9ca6cbc
--- /dev/null
+++ b/include/kirq.h
@@ -0,0 +1,117 @@
+/**
+ * @file kirq.h
+ * @author Giovanni Bajo
+ * @brief Kernel IRQ Wait Functions
+ * @ingroup kernel
+ *
+ * This module contains functions that allow to wait for a specific interrupt
+ * to be triggered, while yielding execution to other threads.
+ *
+ * This can be useful for all situations where blocking code is waiting for the
+ * hardware to perform some operations. Normally, the hardware reports back
+ * completion of the operation by triggering an interrupt, so it can be useful
+ * to use the CPU time to execute other threads while waiting for the interrupt.
+ *
+ * All functions are designed to be nops when the kernel is not initialized,
+ * so that the spin loops will also work without multithreading (they just
+ * won't be able to yield).
+ */
+
+#ifndef LIBDRAGON_KERNEL_KIRQ_H
+#define LIBDRAGON_KERNEL_KIRQ_H
+
+#include
+#include "preview.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+///@cond
+typedef struct kcond_s kcond_t;
+///@endcond
+
+/** @brief Kirq waiting structure
+ *
+ * Use on of the kirq_begin_wait functions to obtain an object of this
+ * type, and then pass it to kirq_wait to wait for the interrupt (one or
+ * multiple times).
+ */
+typedef struct {
+ int64_t counter;
+ kcond_t *cond;
+} kirq_wait_t;
+
+/**
+ * @brief Create an kirq wait object for SP interrupts
+ * @preview
+ *
+ * @return The wait object
+ */
+LIBDRAGON_PREVIEW_API
+kirq_wait_t kirq_begin_wait_sp(void);
+
+/**
+ * @brief Create an kirq wait object for DP interrupts
+ * @preview
+ *
+ * @return The wait object
+ */
+LIBDRAGON_PREVIEW_API
+kirq_wait_t kirq_begin_wait_dp(void);
+
+/**
+ * @brief Create an kirq wait object for SI interrupts
+ * @preview
+ *
+ * @return The wait object
+ */
+LIBDRAGON_PREVIEW_API
+kirq_wait_t kirq_begin_wait_si(void);
+
+/**
+ * @brief Create an kirq wait object for AI interrupts
+ * @preview
+ *
+ * @return The wait object
+ */
+LIBDRAGON_PREVIEW_API
+kirq_wait_t kirq_begin_wait_ai(void);
+
+/**
+ * @brief Create an kirq wait object for VI interrupts
+ * @preview
+ *
+ * @return The wait object
+ */
+LIBDRAGON_PREVIEW_API
+kirq_wait_t kirq_begin_wait_vi(void);
+
+/**
+ * @brief Create an kirq wait object for PI interrupts
+ * @preview
+ *
+ * @return The wait object
+ */
+LIBDRAGON_PREVIEW_API
+kirq_wait_t kirq_begin_wait_pi(void);
+
+/**
+ * @brief Wait until the interrupt is triggered
+ * @preview
+ *
+ * This function will block the current thread until the interrupt
+ * is triggered. It can be called multiple times on the same wait object,
+ * to wait for further interrupts, and it guarantees that none of them
+ * will be missed.
+ *
+ * @param wait
+ */
+LIBDRAGON_PREVIEW_API
+void kirq_wait(kirq_wait_t *wait);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/kqueue.h b/include/kqueue.h
new file mode 100644
index 000000000..9a7e04782
--- /dev/null
+++ b/include/kqueue.h
@@ -0,0 +1,188 @@
+/**
+ * @file kqueue.h
+ * @author Giovanni Bajo
+ */
+#ifndef LIBDRAGON_KERNEL_KQUEUE_H
+#define LIBDRAGON_KERNEL_KQUEUE_H
+
+#include
+#include
+#include "preview.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief A thread-safe FIFO queue
+ *
+ * This structure is used to create a thread-safe FIFO queue. The queue
+ * is implemented as a circular buffer of void* pointers.
+ *
+ * It is possible to enqueue and dequeue elements from the queue, and
+ * the queue will block if the queue is full or empty, respectively.
+ *
+ * The size of the queue is fixed at creation time, and cannot be changed
+ * afterwards.
+ */
+typedef struct kqueue_s kqueue_t;
+
+/**
+ * @brief Create a new queue
+ * @preview
+ *
+ * This function creates a new queue with the specified size. The size
+ * is the number of elements that the queue can hold.
+ *
+ * @param[in] size Size of the queue in number of elements
+ *
+ * @return A pointer to the queue structure created
+ */
+LIBDRAGON_PREVIEW_API
+kqueue_t *kqueue_new(int size);
+
+/**
+ * @brief Destroy a queue
+ * @preview
+ *
+ * This function destroys a queue, freeing all the resources associated
+ * with it.
+ *
+ * @param[in] queue Pointer to the queue structure
+ */
+LIBDRAGON_PREVIEW_API
+void kqueue_destroy(kqueue_t *queue);
+
+/**
+ * @brief Add an element to the queue
+ * @preview
+ *
+ * This function adds an element to the queue. If the queue is full, the
+ * function will block until there is space in the queue.
+ *
+ * @param[in] queue Pointer to the queue structure
+ * @param[in] element Pointer to the element to add
+ */
+LIBDRAGON_PREVIEW_API
+void kqueue_put(kqueue_t *queue, void *element);
+
+/**
+ * @brief Attempt to add an element to a queue from an interrupt context
+ * @preview
+ *
+ * @param[in] queue Pointer to the queue structure
+ * @param[in] element Pointer to the element to add
+ *
+ * @return true if the element was added to the queue, false otherwise
+ */
+LIBDRAGON_PREVIEW_API
+bool kqueue_try_put_isr(kqueue_t *queue, void *element);
+
+/**
+ * @brief Try to add an element to the queue with timeout
+ * @preview
+ *
+ * This function adds an element to the queue. If the queue is full, the
+ * function will block until there is space in the queue or the timer
+ * expires.
+ *
+ * @param[in] queue Pointer to the queue structure
+ * @param[in] element Pointer to the element to add
+ * @param[in] ticks Number of hardware ticks to wait
+ *
+ * @return true if the element was added to the queue, false on timeout
+ */
+LIBDRAGON_PREVIEW_API
+bool kqueue_try_put(kqueue_t *queue, void *element, uint32_t ticks);
+
+/**
+ * @brief Remove an element from the queue
+ * @preview
+ *
+ * This function removes an element from the queue. If the queue is empty,
+ * the function will block until there is an element in the queue.
+ *
+ * @param[in] queue Pointer to the queue structure
+ *
+ * @return Pointer to the element removed
+ */
+LIBDRAGON_PREVIEW_API
+void *kqueue_get(kqueue_t *queue);
+
+/**
+ * @brief Try to remove an element from the queue with timeout
+ * @preview
+ *
+ * This function removes an element from the queue. If the queue is empty,
+ * the function will block until there is an element in the queue or the
+ * timeout expires.
+ *
+ * @param[in] queue Pointer to qeueue structure
+ * @param[out] element Pointer pointer to element
+ * @param[in] ticks Number of hardware ticks to wait
+ *
+ * @return true if the element was fetched from queue, false on timeout
+ */
+LIBDRAGON_PREVIEW_API
+bool kqueue_try_get(kqueue_t *queue, void **element, uint32_t ticks);
+
+/**
+ * @brief Get the number of elements in the queue
+ * @preview
+ *
+ * @param[in] queue Pointer to the queue structure
+ *
+ * @return The number of elements in the queue
+ */
+LIBDRAGON_PREVIEW_API
+int kqueue_count(kqueue_t *queue);
+
+/**
+ * @brief Get the size of the queue
+ * @preview
+ *
+ * @param[in] queue Pointer to the queue structure
+ *
+ * @return The size of the queue
+ */
+LIBDRAGON_PREVIEW_API
+int kqueue_size(kqueue_t *queue);
+
+/**
+ * @brief Check if the queue is empty
+ * @preview
+ *
+ * @param[in] queue Pointer to the queue structure
+ *
+ * @return true if the queue is empty, false otherwise
+ */
+LIBDRAGON_PREVIEW_API
+bool kqueue_empty(kqueue_t *queue);
+
+/**
+ * @brief Check if the queue is full
+ * @preview
+ *
+ * @param[in] queue Pointer to the queue structure
+ *
+ * @return true if the queue is full, false otherwise
+ */
+LIBDRAGON_PREVIEW_API
+bool kqueue_full(kqueue_t *queue);
+
+/**
+ * @brief Peek at the element at the head of the queue
+ * @preview
+ *
+ * @param[in] queue Pointer to the queue structure
+ *
+ * @return Element at the head of the queue, or NULL if the queue is empty
+ */
+LIBDRAGON_PREVIEW_API
+void *kqueue_peek(kqueue_t *queue);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/ksemaphore.h b/include/ksemaphore.h
new file mode 100644
index 000000000..237e2d6f2
--- /dev/null
+++ b/include/ksemaphore.h
@@ -0,0 +1,117 @@
+/**
+ * @file ksemaphore.h
+ * @author Giovanni Bajo
+ * @brief Kernel Semaphore prmitive
+ * @ingroup kernel
+ *
+ */
+
+#ifndef LIBDRAGON_KERNEL_KSEMAPHORE_H
+#define LIBDRAGON_KERNEL_KSEMAPHORE_H
+
+#include
+#include "preview.h"
+#include "kernel.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** @brief Kernel Semaphore structure
+ *
+ * This structure represents a semaphore in the kernel. Semaphores are
+ * used to synchronize access to shared resources between threads.
+ */
+typedef struct {
+ kmutex_t mutex;
+ kcond_t cond;
+ int count;
+} ksemaphore_t;
+
+/**
+ * @brief Initialize a semaphore
+ * @preview
+ *
+ * This function initializes a semaphore with the specified value.
+ *
+ * @param[in] sem Pointer to the semaphore structure
+ * @param[in] value Initial value of the semaphore
+ */
+LIBDRAGON_PREVIEW_API
+void ksemaphore_init(ksemaphore_t *sem, int value);
+
+/**
+ * @brief Destroy a semaphore
+ * @preview
+ *
+ * This function destroys a semaphore, freeing all the resources
+ * associated with it.
+ *
+ * @param[in] sem Pointer to the semaphore structure
+ */
+LIBDRAGON_PREVIEW_API
+void ksemaphore_destroy(ksemaphore_t *sem);
+
+/**
+ * @brief Post to a semaphore
+ * @preview
+ *
+ * This function increments the value of a semaphore and signals
+ * any threads waiting on the semaphore.
+ *
+ * @param sem Pointer to the semaphore structure
+ */
+LIBDRAGON_PREVIEW_API
+void ksemaphore_post(ksemaphore_t *sem);
+
+/**
+ * @brief Post to a semaphore from an interrupt context
+ * @preview
+ *
+ * This function increments the value of the semaphore and signals
+ * a thread waiting on the semaphore.
+ *
+ * @param sem Pointer to the semaphore structure
+ */
+LIBDRAGON_PREVIEW_API
+void ksemaphore_post_isr(ksemaphore_t *sem);
+
+/**
+ * @brief Wait on a semaphore
+ * @preview
+ *
+ * This function waits on a semaphore. If the semaphore's value is
+ * greater than 0, the function will decrement the value and return
+ * immediately. If the value is 0, the function will block until
+ * the semaphore's value is greater than 0.
+ *
+ * @param[in] sem Pointer to the semaphore structure
+ */
+LIBDRAGON_PREVIEW_API
+void ksemaphore_wait(ksemaphore_t *sem);
+
+/**
+ * @brief Try to wait on a semaphore, up to a timeout
+ * @preview
+ *
+ * This function tries to wait on a semaphore, up to a specified
+ * timeout. If the semaphore's value is greater than 0, the function
+ * will decrement the value and return true. If the value is 0, the
+ * function will block until the semaphore's value is greater than 0,
+ * or the timeout expires.
+ *
+ * As a special case, if the timeout is 0, the function will not block.
+ *
+ * @param sem Pointer to the semaphore structure
+ * @param ticks Timeout in ticks
+ * @return true If the semaphore was successfully waited on
+ * @return false If the semaphore was not successfully waited on
+ */
+LIBDRAGON_PREVIEW_API
+bool ksemaphore_try_wait(ksemaphore_t *sem, uint32_t ticks);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/ktls.h b/include/ktls.h
new file mode 100644
index 000000000..751200fda
--- /dev/null
+++ b/include/ktls.h
@@ -0,0 +1,21 @@
+/**
+ * @file ktls.h
+ * @author Liam Coleman
+ * @author Giovanni Bajo
+ */
+#ifndef LIBDRAGON_KERNEL_TLS_H
+#define LIBDRAGON_KERNEL_TLS_H
+
+#ifndef __ASSEMBLER__
+/** @brief Assembly macro for thread-local storage access */
+__asm__ (
+ ".ifndef __RDHWR_WAS_DEFINED" "\n"
+ ".macro rdhwr rt, rd" "\n"
+ " lw \\rt, %gprel(__th_cur_tp)($gp)" "\n"
+ ".endm" "\n"
+ ".set __RDHWR_WAS_DEFINED, 1" "\n"
+ ".endif" "\n"
+);
+#endif
+
+#endif
diff --git a/include/libdragon.h b/include/libdragon.h
index 6ed9046fe..88e115906 100755
--- a/include/libdragon.h
+++ b/include/libdragon.h
@@ -22,38 +22,70 @@
*/
/* Easy include wrapper */
+#include "preview.h"
#include "n64types.h"
#include "fmath.h"
+#include "fgeom.h"
+#include "fgeom2d.h"
#include "audio.h"
+#include "entropy.h"
#include "console.h"
+#include "emux.h"
#include "debug.h"
+#include "fat.h"
#include "joybus.h"
+#include "joybus_accessory.h"
+#include "pixelfx.h"
#include "joypad.h"
#include "controller.h"
#include "rtc.h"
+#include "bio_sensor.h"
#include "mempak.h"
+#include "cpak.h"
+#include "cpakfs.h"
+#include "vi.h"
+#include "eia608.h"
#include "tpak.h"
#include "display.h"
#include "dma.h"
#include "dragonfs.h"
#include "asset.h"
+#include "pifile.h"
#include "eeprom.h"
#include "eepromfs.h"
+#include "flashram.h"
#include "graphics.h"
#include "mi.h"
#include "interrupt.h"
+#include "kernel.h"
+#include "kirq.h"
+#include "kqueue.h"
+#include "ksemaphore.h"
#include "n64sys.h"
+#include "scratch.h"
+#include "vaddr64.h"
+#include "dd.h"
#include "backtrace.h"
#include "rdp.h"
#include "rsp.h"
#include "timer.h"
#include "exception.h"
#include "dir.h"
+#include "yuv.h"
+#include "subtitles.h"
+#include "fmv.h"
+#include "video.h"
+#include "mpeg1.h"
+#include "h264.h"
#include "mixer.h"
#include "samplebuffer.h"
#include "wav64.h"
#include "xm64.h"
#include "ym64.h"
+#include "sf64.h"
+#include "sf64_synth.h"
+#include "midi_target.h"
+#include "mid64.h"
#include "rspq.h"
#include "rdpq.h"
#include "rdpq_tri.h"
@@ -67,10 +99,25 @@
#include "rdpq_font.h"
#include "rdpq_debug.h"
#include "rdpq_macros.h"
+#include "rdpq_mat.h"
+#include "rdpq_xform.h"
#include "surface.h"
#include "sprite.h"
+#include "lspr3.h"
+#include "lspr1.h"
#include "debugcpp.h"
#include "dlfcn.h"
+#include "model64.h"
+#include "skc.h"
+#include "nand.h"
+#include "bbfs.h"
#include "ioctl.h"
+#include "a3d.h"
+#include "profile.h"
+#include "coroutine.h"
+#include "magma.h"
+#include "magma_math.h"
+#include "mgfx.h"
+#include "sram.h"
#endif
diff --git a/include/lspr1.h b/include/lspr1.h
new file mode 100644
index 000000000..651550769
--- /dev/null
+++ b/include/lspr1.h
@@ -0,0 +1,61 @@
+/**
+ * @file lspr1.h
+ * @author Giovanni Bajo
+ * @brief Lossy-sprite Level 1: BC1Q decoder
+ *
+ * BC1Q is one of libdragon's lossy sprite formats ("Lossy-sprite Level 1"),
+ * a good fit for large textures where a low decoding cost matters. It is the
+ * lighter-weight alternative to #lspr3_init (Level 3, H264I), which trades a
+ * heavier runtime decoder for higher quality at a given size.
+ *
+ * BC1Q files are produced by `mksprite --lossy= --compress 1` and
+ * decode at load time into a standard #FMT_RGBA16 sprite, so the returned
+ * #sprite_t behaves like any other sprite.
+ *
+ * To load BC1Q sprites you must first opt-in by calling #lspr1_init, which
+ * registers the decoder with #sprite_load. After that, #sprite_load recognizes
+ * BC1Q sprites and decodes them transparently; call #lspr1_close when no longer
+ * needed.
+ *
+ * Internally, the format is based on BC1 / DXT1 block compression, and is
+ * decoded using the RSP to accelerate the process.
+ */
+#ifndef __LIBDRAGON_LSPR1_H
+#define __LIBDRAGON_LSPR1_H
+
+#include "preview.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Register the BC1Q (Lossy-sprite Level 1) decoder with the sprite loader.
+ * @preview
+ *
+ * After registering, #sprite_load recognizes the `BC1Q` magic and decodes
+ * matching files transparently. Until then, loading a BC1Q file via
+ * #sprite_load fails with an assertion.
+ *
+ * Refcounted; calling this increments the refcount and will require the
+ * same number of #lspr1_close calls to fully unregister.
+ */
+LIBDRAGON_PREVIEW_API
+void lspr1_init(void);
+
+/**
+ * @brief Unregister the BC1Q (Lossy-sprite Level 1) decoder.
+ * @preview
+ *
+ * Refcounted; calling this decrements the refcount and once it reaches zero,
+ * #sprite_load no longer recognizes BC1Q files and attempts to load them will
+ * fail with an assertion.
+ */
+LIBDRAGON_PREVIEW_API
+void lspr1_close(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/lspr3.h b/include/lspr3.h
new file mode 100644
index 000000000..3ce35883e
--- /dev/null
+++ b/include/lspr3.h
@@ -0,0 +1,65 @@
+/**
+ * @file lspr3.h
+ * @author Giovanni Bajo
+ * @brief Lossy-sprite Level 3: H264I decoder
+ *
+ * H264I ("H.264 Image") is libdragon's "Lossy-sprite Level 3" container format:
+ * lossy compression is useful for large images that need higher storage
+ * compression at the cost of some quality loss, such as large backgrounds. The
+ * quality factor can be tuned at build-time to achieve the desired size/quality
+ * tradeoff. H264I files are produced by `mksprite --lossy= --compress 3`
+ * and always decode to a #FMT_RGBA16 sprite.
+ *
+ * You must opt-in to support for Level 3 lossy sprites by calling #lspr3_init
+ * to register the H264I decoder with #sprite_load before loading H264I files.
+ * Once initialized, #sprite_load will recognize H264I-compressed sprites and
+ * decode them transparently.
+ *
+ * H264I is implemented as an H.264 intra-only bitstream (4:2:0 YUV, BT.709
+ * full range) plus a small header. At load-time the H.264 slice is decoded
+ * and the YUV reconstruction is converted into a #FMT_RGBA16 sprite, so the
+ * returned #sprite_t works like a normal sprite.
+ *
+ * H264I can encode either images with no alpha channel, or images with a
+ * 1-bit alpha channel. The alpha channel is stored lossless alongside the
+ * H.264 bitstream, and reconstructed at load-time.
+ */
+#ifndef __LIBDRAGON_LSPR3_H
+#define __LIBDRAGON_LSPR3_H
+
+#include "preview.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Register the H264I (Lossy-sprite Level 3) decoder with the sprite loader.
+ * @preview
+ *
+ * After registering, #sprite_load recognizes the `H264` magic and decodes
+ * matching files transparently. Until then, loading an H264I file via
+ * #sprite_load fails with an assertion.
+ *
+ * Refcounted; calling this increments the refcount and will require the
+ * same number of #lspr3_close calls to fully unregister.
+ */
+LIBDRAGON_PREVIEW_API
+void lspr3_init(void);
+
+/**
+ * @brief Unregister the H264I (Lossy-sprite Level 3) decoder.
+ * @preview
+ *
+ * Refcounted; calling this decrements the refcount and once it reaches zero,
+ * #sprite_load no longer recognizes H264I files and attempts to load them will
+ * fail with an assertion.
+ */
+LIBDRAGON_PREVIEW_API
+void lspr3_close(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/magma.h b/include/magma.h
new file mode 100644
index 000000000..b180a7211
--- /dev/null
+++ b/include/magma.h
@@ -0,0 +1,756 @@
+/**
+ * @file magma.h
+ * @author Dennis Heinze
+ * @brief Magma core API
+ * @ingroup magma
+ */
+
+/**
+ * @defgroup magma Magma: Hardware-accelerated 3D graphics API
+ * @brief Interface for transforming and drawing 3D geometry with a focus on performance and customizability.
+ * @ingroup display
+ *
+ * Magma is a library that allows users to perform T&L (https://en.wikipedia.org/wiki/Transform,_clipping,_and_lighting)
+ * of 3D geometry in a highly customizable, hardware-accelerated fashion.
+ *
+ * The key feature of magma that allows for most of its customizability is the support for vertex shaders,
+ * since they can be written to implement any arbitrary method of transformation and lighting (within the limits of the hardware of course).
+ * Vertex shaders run on the RSP ("Reality Signal Processor") and can therefore make use of its special vector instructions
+ * to speed up many of the necessary matrix and vector calculations.
+ *
+ * The transformed and shaded triangles that result from the vertex shader are then directly sent to the RDP ("Reality Display Processor")
+ * for rasterization without a roundtrip to the CPU. Other than that, magma does not communicate with the RDP. To configure things like
+ * render modes, textures, etc. the @ref rdpq library should be used.
+ *
+ * Magma uses the rspq library (see @ref rspq.h) to interface with the RSP internally. In fact, many of magma's functions are just
+ * wrappers around rspq commands and therefore support being recorded into rspq blocks (#rspq_block_t). These functions are marked as
+ * such in their documentation with the sentence "Can be recorded into blocks".
+ *
+ * ## Overview of magma's architecture
+ *
+ * This is a list of frequently used terms and their definitions:
+ *
+ * ### Vertex shader
+ * This is a special type of ucode (#rsp_ucode_t) that implements the magma vertex shader interface. It contains some metadata to describe itself to magma,
+ * and implements the vertex processing loop to perform the T&L. Magma comes with a builtin default shader that is fit for most purposes: @ref mgfx.h
+ *
+ * ### Vertex input
+ * Every vertex shader defines a set of vertex inputs. A vertex input is some value that a shader expects per vertex, for example its
+ * position, color, texture coordinates etc. Each vertex input is identified with an "input number", which is unique within the shader.
+ * The shader may define certain inputs to be optional.
+ *
+ * ### Vertex layout
+ * While the set of a shader's vertex inputs can be thought of as an interface, a vertex layout is like an implementation of that interface.
+ * It is the physical memory layout of some vertex data that is passed to the vertex shader as input. Shaders may be configured to accept
+ * a range of different vertex layouts as their input, provided they are compatible. See #mg_vertex_layout_t for details.
+ *
+ * ### Pipeline
+ * A pipeline is an instance of a vertex shader that has been configured with a certain vertex layout. To actually draw any 3D geometry,
+ * a pipeline must first be created and then "bound". After a pipeline has been bound, all subsequent drawing commands will use its vertex shader.
+ * A program can create as many pipelines as desired and switch between them at will by binding them. This is useful for both supporting
+ * multiple vertex layouts and achieving different visual effects. See #mg_pipeline_t.
+ *
+ * ### Uniform
+ * Besides vertex inputs, a vertex shader also defines a set of uniforms. Uniforms are inputs to the shader that don't change per vertex.
+ * Common examples are matrices and lights. Like vertex inputs, they have a unique identifier each, called the "binding number".
+ * Uniforms can be queried from a pipeline using the binding number. To make the uniform's values accessible to the vertex shader,
+ * they need to reside in the RSP's DMEM. That means to assign a value to a uniform, it must be loaded first. See #mg_uniform_t for details.
+ *
+ * ### Vertex buffer
+ * A vertex buffer is the source of vertex data that is to be transformed by a vertex shader. It is just a regular region of memory in RDRAM
+ * and no special functions are required to create it. A vertex buffer must be "bound" to make its contents available to a vertex shader.
+ * See #mg_bind_vertex_buffer for details.
+ *
+ * ### Vertex cache
+ * This is an internal memory buffer that the results of the vertex shader are stored into. Drawing geometry with magma actually happens in
+ * two distinct steps: First, vertices are "loaded" into the vertex cache, running them through the vertex shader in the process.
+ * Triangles can then be drawn by referencing the tranformed vertices inside the cache. This way, vertices don't need to be transformed
+ * redundantly in case they are shared by multiple triangles. See #mg_load_vertices and #mg_draw_triangle for details.
+ *
+ * ### Drawing command
+ * These are functions that will actually draw geometry to the framebuffer. The most simple example is #mg_draw_triangle, which will draw
+ * a single triangle. There are also more advanced drawing commands which can draw entire meshes at once: #mg_draw and #mg_draw_indexed.
+ * Internally they will just call #mg_load_vertices and #mg_draw_triangle repeatedly until the entire mesh has been drawn.
+ * Translating a list of indices to a sequence of vertex loads and triangle draws is expensive though, so it's best practice to record
+ * them into rspq blocks (#rspq_block_t) if the indices don't change.
+ *
+ * ### Fixed function states
+ * There are some states that are built into magma. They can be set at any time to configure how geometry is being drawn.
+ * Some of them are related to drawing triangles only, others are accessible to every vertex shader automatically.
+ * This is the list of functions that set these states. See their respective documentation for more information:
+ * * #mg_set_viewport
+ * * #mg_set_geometry_flags
+ * * #mg_set_culling
+ * * #mg_set_clip_factor
+ *
+ * ## How to draw geometry: Quick guide
+ * At initialization:
+ * 1. Initialize magma using #mg_init.
+ * 2. Create a pipeline from a vertex shader (for example mgfx) and a vertex layout that corresponds to your vertex data using #mg_pipeline_create.
+ * 3. Query uniforms from the pipeline with #mg_pipeline_get_uniform.
+ *
+ * During the rendering loop:
+ * 1. Bind the pipeline using #mg_pipeline_bind.
+ * 2. Load uniforms using e.g. #mg_uniform_load.
+ * 3. Set fixed function states:
+ * 1. The viewport, which is the area that geometry is drawn to (#mg_set_viewport).
+ * 2. Geometry flags, which specify what type of triangles should be sent to the RDP (#mg_set_geometry_flags).
+ * 3. The face culling mode (#mg_set_culling).
+ * 4. Bind a pointer to your vertex data as the vertex buffer with #mg_bind_vertex_buffer.
+ * 5. Draw the geometry by using one of the drawing commands, or by calling a rspq block with recorded drawing commands.
+ *
+ * The order of steps in the rendering loop is not fixed, with some exceptions. Step 5 should obviously always happen last.
+ * Step 2 should usually happen after step 1, though there are exceptions (see #mg_pipeline_bind).
+ *
+ * If there are different objects in your scene that share some properties, not all of the above steps need to be repeated for each object.
+ * For example: If two objects use the same "material" (same vertex shader, same textures etc.) but only differ in their position, it is sufficient
+ * to, after the first object has been drawn, only load the uniform that corresponds to "position" before drawing the second object.
+ * Or you could have two objects that share their geometry, but use a different vertex shader. In this case you would have created two pipelines
+ * at initialization. After drawing the first object, you would bind the second pipeline, load its uniforms, and then draw the second object
+ * without re-binding the vertex buffer.
+ *
+ * To put the above in more formal terms: Most functions that set some state work orthogonally to each other. The only exception is uniform loading
+ * with respect to pipeline binding, since uniforms may be invalidated after binding a pipeline. With respect to other uniforms and other states,
+ * uniform loading can still be viewed as orthogonal.
+ *
+ * The "mghello" example demonstrates the above steps in a minimal fashion. A more complicated example can be found in "mgdemo".
+ */
+
+#ifndef __LIBDRAGON_MAGMA_H
+#define __LIBDRAGON_MAGMA_H
+
+
+#include
+#include
+#include "preview.h"
+#include "graphics.h"
+#include "display.h"
+#include "rsp.h"
+#include "rspq.h"
+#include "magma_types.h"
+#include "debug.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Initialize the magma library.
+ * @preview
+ *
+ * This function must be called before any other functions of this library are used.
+ * It is safe to call this function multiple times. If the library is already
+ * initialized, this function has no effect.
+ */
+LIBDRAGON_PREVIEW_API
+void mg_init(void);
+
+/**
+ * @brief Shut down the magma library.
+ * @preview
+ *
+ * It is safe to call this function multiple times. If the library is not
+ * initialized, this function has no effect.
+ */
+LIBDRAGON_PREVIEW_API
+void mg_close(void);
+
+/**
+ * @brief Creates a new pipeline from a vertex shader and vertex layout.
+ * @preview
+ *
+ * A copy of the ucode will be created internally, which is then patched automatically
+ * to work with the specified vertex layout (provided it is compatible).
+ *
+ * See #mg_pipeline_parms_t and #mg_vertex_layout_t for more details.
+ * Must be freed with #mg_pipeline_free.
+ *
+ * @param[in] parms Pointer to a struct containing parameters for creating a pipeline.
+ * @return The newly created pipeline.
+ *
+ * @see #mg_pipeline_parms_t
+ * @see #mg_vertex_layout_t
+ * @see #mg_pipeline_free
+ */
+LIBDRAGON_PREVIEW_API
+mg_pipeline_t *mg_pipeline_create(const mg_pipeline_parms_t *parms);
+
+/**
+ * @brief Destructs and frees a pipeline.
+ * @preview
+ *
+ * Must not be called while any drawing operations using this pipeline are still ongoing.
+ *
+ * @param[in] pipeline The pipeline to be freed.
+ */
+LIBDRAGON_PREVIEW_API
+void mg_pipeline_free(mg_pipeline_t *pipeline);
+
+/**
+ * @brief Returns a struct describing the uniform with the given binding number.
+ * @preview
+ *
+ * The struct can be used directly to load uniform values using #mg_uniform_load or #mg_uniform_load_inline
+ * This function crashes if no uniform with the given binding number exists in the pipeline.
+ * Note that binding numbers are not required to be contiguous.
+ *
+ * @param[in] pipeline The pipeline to query the uniform from.
+ * @param[in] binding The binding number of the uniform that should be queried.
+ * @return The uniform in the pipeline with the specified binding number.
+ *
+ * @see #mg_uniform_load
+ * @see #mg_uniform_load_inline
+ */
+LIBDRAGON_PREVIEW_API
+const mg_uniform_t *mg_pipeline_get_uniform(mg_pipeline_t *pipeline, uint32_t binding);
+
+/**
+ * @brief Checks if a uniform is compatible with the pipeline.
+ * @preview
+ *
+ * If the uniform has been queried from the pipeline using #mg_pipeline_get_uniform,
+ * it is by definition always compatible.
+ *
+ * If the uniform has been queried from a different pipeline that was created with the
+ * same vertex shader, it is guaranteed to be compatible.
+ *
+ * Uniforms from other pipelines that do not share vertex shaders may still be compatible
+ * if this pipeline contains a uniform with the same binding number which is located in
+ * the same region of memory.
+ *
+ * @param[in] pipeline The pipeline to be checked for compatibility with the uniform.
+ * @param[in] uniform The uniform to be checked for compatibility with the pipeline.
+ * @return True if the uniform is compatible with the pipeline, otherwise false.
+ *
+ * @see #mg_pipeline_get_uniform
+ */
+LIBDRAGON_PREVIEW_API
+bool mg_pipeline_is_uniform_compatible(mg_pipeline_t *pipeline, const mg_uniform_t *uniform);
+
+/**
+ * @brief Bind the pipeline for subsequent use.
+ * @preview
+ *
+ * All following drawing commands will use the newly bound pipeline for transforming vertices.
+ * This function must be called at least once before any drawing commands are issued.
+ *
+ * Binding a pipeline will invalidate any previously loaded uniforms that are not compatible with the pipeline.
+ * This compatibility can be checked using #mg_pipeline_is_uniform_compatible.
+ * Inversely this means that compatible uniforms will not be invalidated and their values will be preserved
+ * after binding the new pipeline.
+ *
+ * Can be recorded into blocks.
+ *
+ * @param[in] pipeline The pipeline to be bound.
+ *
+ * @see #mg_pipeline_is_uniform_compatible
+ */
+LIBDRAGON_PREVIEW_API
+void mg_pipeline_bind(mg_pipeline_t *pipeline);
+
+/**
+ * @brief Load data from the given pointer into a uniform.
+ *
+ * The data will not be read immediately. Instead, this operation is performed asynchronously
+ * in the background. This means that the source data must remain valid until the loading has actually
+ * been performed. This will generally be the case after the current frame has been fully drawn.
+ * If you need to load different data to the same uniform every frame, it is advised to keep
+ * multiple source buffers and alternate between them. A good default for the number of buffers is the
+ * number of frame buffers that was passed to #display_init.
+ *
+ * The actual loading is performed by a DMA operation. That means you need to ensure that
+ * the source data is either in uncached memory, or has been flushed back to RDRAM before calling this function.
+ *
+ * This function only has defined behavior if the given uniform descriptor has been queried
+ * from the pipeline that is currently bound (see #mg_pipeline_bind), or is compatible with
+ * the currently bound pipeline. The latter can be checked using #mg_pipeline_is_uniform_compatible.
+ *
+ * Can be recorded into blocks. Note that only the pointer value itself will be recorded.
+ * Therefore it is still possible to modify the data afterwards and have it correctly loaded.
+ *
+ * @param[in] uniform The uniform that the data should be loaded into.
+ * @param[in] data Pointer to the data to be loaded.
+ * The number of bytes that will be read is equal to the size of the uniform.
+ *
+ * @see #mg_pipeline_bind
+ * @see #mg_pipeline_is_uniform_compatible
+ */
+inline void mg_uniform_load(const mg_uniform_t *uniform, const void *data);
+
+/**
+ * @brief Load data from the given pointer into uniform memory.
+ * @preview
+ *
+ * This function works like #mg_uniform_load but takes the offset and size of the memory region to be loaded into directly.
+ * It should be used with care, since it is possible to corrupt the system's state permanently
+ * and cause crashes by specifying an invalid region. The region must not exceed the size of the
+ * pipeline's uniform memory, which is defined as the offset plus size of the pipeline's "last" uniform.
+ * Note that the last uniform is that with the largest offset, and not necessarily that with the largest binding number.
+ *
+ * Can be recorded into blocks.
+ *
+ * @param[in] offset The offset in bytes of the region in uniform memory that data should be loaded to.
+ * @param[in] size The size in bytes of the region in uniform memory that data should be loaded to.
+ * @param[in] data Pointer to the data to be loaded.
+ *
+ * @see #mg_uniform_load
+ */
+LIBDRAGON_PREVIEW_API
+void mg_uniform_load_raw(uint32_t offset, uint32_t size, const void *data);
+
+/**
+ * @brief Load inline data into a uniform.
+ *
+ * This works just like #mg_uniform_load, except that the data is read immediately
+ * and embedded into the internal stream of commands. This means there is no need
+ * to keep the data around until the asynchronous loading operation is complete.
+ * However, this function comes with more performance overhead compared to #mg_uniform_load.
+ *
+ * @param[in] uniform The uniform that the data should be loaded into.
+ * @param[in] data Pointer to the data to be embedded and then loaded.
+ * The number of bytes that will be read is equal to the size of the uniform.
+ *
+ * @see #mg_uniform_load
+ */
+inline void mg_uniform_load_inline(const mg_uniform_t *uniform, const void *data);
+
+/**
+ * @brief Load inline data into uniform memory.
+ * @preview
+ *
+ * This works just like #mg_uniform_load_raw, except that the data is read immediately
+ * and embedded into the internal stream of commands. This means there is no need
+ * to keep the data around until the asynchronous loading operation is complete.
+ * However, this function comes with more performance overhead compared to #mg_uniform_load_raw.
+ *
+ * Can be recorded into blocks.
+ *
+ * @param[in] offset The offset in bytes of the region in uniform memory that data should be loaded to.
+ * @param[in] size The size in bytes of the region in uniform memory that data should be loaded to.
+ * @param[in] data Pointer to the data to be embedded and then loaded.
+ *
+ * @see #mg_uniform_load_raw
+ */
+LIBDRAGON_PREVIEW_API
+void mg_uniform_load_inline_raw(uint32_t offset, uint32_t size, const void *data);
+
+/**
+ * @brief Set the culling mode for 3D geometry.
+ *
+ * Use this function to specify whether to cull back faces, front faces or turn off culling.
+ * It is also possible to configure the winding direction of triangles.
+ * See #mg_culling_parms_t for details.
+ *
+ * Can be recorded into blocks.
+ *
+ * @param[in] culling Pointer to a struct containing the culling configuration.
+ *
+ * @see #mg_culling_parms_t
+ */
+inline void mg_set_culling(const mg_culling_parms_t *culling);
+
+/**
+ * @brief Set the geometry flags for 3D geometry.
+ *
+ * The geometry flags specify which attributes should be transmitted to the RDP, the hardware rasterizer.
+ * See #mg_geometry_flags_t for details.
+ *
+ * Can be recorded into blocks.
+ *
+ * @param[in] flags Geometry flags.
+ *
+ * @see #mg_geometry_flags_t
+ */
+inline void mg_set_geometry_flags(mg_geometry_flags_t flags);
+
+/**
+ * @brief Set the viewport, which is the region in screen space that geometry will be drawn to.
+ * @preview
+ *
+ * Can be recorded into blocks.
+ *
+ * @param[in] viewport Pointer to a struct containing the viewport configuration.
+ *
+ * @see #mg_viewport_t
+ * @see #mg_set_viewport_fullscreen
+ */
+LIBDRAGON_PREVIEW_API
+void mg_set_viewport(const mg_viewport_t *viewport);
+
+/**
+ * @brief Set the viewport to fullscreen, using the provided display resolution.
+ *
+ * Can be recorded into blocks.
+ *
+ * @param[in] res The display resolution.
+ *
+ * @see #mg_viewport_t
+ * @see #mg_set_viewport
+ */
+inline void mg_set_viewport_fullscreen(resolution_t res);
+
+/**
+ * @brief Set the clipping guard factor.
+ *
+ * This factor specifies the size of the clipping guard band in multiples of the viewport size.
+ * The clipping guard band is a screen space region centered around the actual viewport.
+ * Triangles which stick outside the viewport but are still within the guard band will not be clipped
+ * as a performance improvement, since clipping is very costly.
+ *
+ * A factor of 1 will effectively disable the clipping guard, since it will be exactly as big as the viewport itself.
+ *
+ * The default value is #MG_DEFAULT_GUARD_BAND.
+ *
+ * Can be recorded into blocks.
+ *
+ * @param[in] factor The clipping guard size factor.
+ */
+inline void mg_set_clip_factor(uint32_t factor);
+
+/**
+ * @brief Bind the vertex buffer to be used by subsequent vertex loading commands.
+ *
+ * All vertex load commands (issued using #mg_load_vertices) issued after this command will
+ * use the specified buffer as their source, until this function is called again with
+ * a different buffer.
+ *
+ * Vertex loading commands take as parameter an offset into the vertex buffer as opposed to a direct pointer.
+ * When they are executed, they will load vertices at that offset from the pointer that was given to this function.
+ * Note that it is therefore possible to swap out the vertex buffer without needing to adjust vertex loading commands,
+ * for example if they were recorded into a block.
+ *
+ * Care must be taken that the source data remains valid until all vertex loading commands that use it have been completed.
+ *
+ * Vertex loading commands use hardware DMAs internally, which means the vertex data must
+ * either be located in uncached memory or flushed back to RDRAM before calling this function.
+ *
+ * Can be recorded into blocks.
+ *
+ * @param[in] buffer Pointer to the vertex buffer to be used by subsequent vertex loading commands.
+ *
+ * @see #mg_load_vertices
+ */
+inline void mg_bind_vertex_buffer(const void *buffer);
+
+/**
+ * @brief Begin a batch of drawing commands.
+ * @preview
+ *
+ * This function must be called before a batch of drawing commands that use the same render modes.
+ * Use #mg_draw_end to end this batch.
+ *
+ * Render modes are configured using the @ref rdpq library. Magma's fixed function settings such as #mg_set_geometry_flags are
+ * not required to be separated from drawing batches. There are some exceptions in rdpq as well, such as #rdpq_set_prim_color.
+ *
+ * To ensure proper synchronization of render modes, these rules should be followed:
+ * * Functions that change render modes must not be called
+ * between a pair of calls to #mg_draw_begin and #mg_draw_end in that order.
+ * * Functions that issue drawing commands must only be called
+ * between a pair of calls to #mg_draw_begin and #mg_draw_end in that order.
+ *
+ * Can be recorded into blocks.
+ *
+ * @see #mg_draw_end
+ */
+LIBDRAGON_PREVIEW_API
+void mg_draw_begin(void);
+
+/**
+ * @brief End a batch of drawing commands.
+ * @preview
+ *
+ * This function must be called after a batch of drawing commands that use the same render modes.
+ * See #mg_draw_begin for more details.
+ *
+ * Can be recorded into blocks.
+ *
+ * @see #mg_draw_begin
+ */
+LIBDRAGON_PREVIEW_API
+void mg_draw_end(void);
+
+/**
+ * @brief Load vertices from the vertex buffer, run the current pipeline's vertex shader on them, and save the result to the vertex cache.
+ * @preview
+ *
+ * The source vertex data will be read starting from the specified index into the vertex buffer.
+ * The transformed vertices will be stored at the specified index into the vertex cache.
+ *
+ * After the transformed vertices have been stored into the cache, they can be used to draw triangles
+ * by issuing drawing commands (see #mg_draw_triangle).
+ *
+ * The cache index plus the vertex count must not exceed the maximum number of vertices that can
+ * be held by the vertex cache, which is #MG_VERTEX_CACHE_COUNT.
+ *
+ * The vertex loading is not carried out immediately after calling this function. Instead, it runs asynchronously
+ * in the background. This means that the vertex source data must remain valid until it has actually been read. This will
+ * generally be the case after the current frame has been fully drawn. If you need to change the vertex data every frame
+ * (for example dynamically generated geometry or "morphing" based animation), it is advised to keep multiple
+ * source buffers and alternate between them. A good default for the number of buffers is the number of
+ * frame buffers that was passed to #display_init. If you only need to apply some linear transformation to the
+ * entire geometry, you should use matrices instead, since they can be stored in uniforms.
+ *
+ * Can be recorded into blocks.
+ *
+ * @param[in] buffer_index The offset into the vertex buffer from which to start loading.
+ * The actual offset in bytes is this number multiplied by the vertex stride of the currently bound pipeline.
+ * @param[in] cache_index The offset into the vertex cache where to store the transformed vertices,
+ * in multiples of the internal vertex size.
+ * @param[in] count The number of vertices to load. The total number of bytes read from the
+ * vertex buffer is this number multiplied by the vertex stride of the currently bound pipeline.
+ *
+ * @see #mg_draw_triangle
+ * @see #MG_VERTEX_CACHE_COUNT
+ */
+LIBDRAGON_PREVIEW_API
+void mg_load_vertices(uint32_t buffer_index, uint8_t cache_index, uint32_t count);
+
+/**
+ * @brief Draw a triangle with vertices that have previously been stored in the vertex cache.
+ * @preview
+ *
+ * The values of all indices must be less than the maximum number of vertices that can be held by
+ * the vertex cache, which is #MG_VERTEX_CACHE_COUNT.
+ *
+ * Use #mg_load_vertices to populate the vertex cache.
+ *
+ * This is a drawing command and must be placed between a pair of calls to #mg_draw_begin and #mg_draw_end.
+ *
+ * Can be recorded into blocks.
+ *
+ * @param[in] index0 The index into the vertex cache at which the first vertex is stored.
+ * @param[in] index1 The index into the vertex cache at which the second vertex is stored.
+ * @param[in] index2 The index into the vertex cache at which the third vertex is stored.
+ *
+ * @see #mg_load_vertices
+ * @see #MG_VERTEX_CACHE_COUNT
+ * @see #mg_draw_begin
+ * @see #mg_draw_end
+ */
+LIBDRAGON_PREVIEW_API
+void mg_draw_triangle(uint8_t index0, uint8_t index1, uint8_t index2);
+
+/**
+ * @brief Draw multiple triangles from consecutive vertices in the vertex buffer.
+ * @preview
+ *
+ * The exact algorithm that is used to construct these triangles is specified by #mg_input_assembly_parms_t.
+ * This function will feed a linear sequence of indices to that algorithm, which starts at the specified first
+ * vertex and has the specified length. In effect this means that triangles are constructed from vertices in
+ * the same order in which they appear in the vertex buffer.
+ *
+ * The algorithm will translate the list of indices to a sequence of multiple vertex loading and
+ * triangle drawing commands. This translation is optimized for efficient loading, as opposed to run time
+ * of the algorithm itself. Therefore it is advised to record the generated commands into a block whenever possible,
+ * to avoid the translation to be reduntantly computed on every frame, even though the result will always be the same.
+ *
+ * This is a drawing command and must be placed between a pair of calls to #mg_draw_begin and #mg_draw_end.
+ *
+ * Can be recorded into blocks.
+ *
+ * @param[in] input_assembly_parms Pointer to a struct containing parameters for input assembly.
+ * @param[in] vertex_count The number of vertices to draw triangles with.
+ * @param[in] first_vertex The index into the vertex buffer to start loading vertices from.
+ *
+ * @see #mg_input_assembly_parms_t
+ * @see #mg_draw_begin
+ * @see #mg_draw_end
+ */
+LIBDRAGON_PREVIEW_API
+void mg_draw(const mg_input_assembly_parms_t *input_assembly_parms, uint32_t vertex_count, uint32_t first_vertex);
+
+/**
+ * @brief Draw multiple triangles from a list of indices that specify offsets into the vertex buffer.
+ * @preview
+ *
+ * The exact algorithm that is used to construct these triangles is specified by #mg_input_assembly_parms_t.
+ * This function will read the specified number of indices from the list and feed them to the algorithm
+ * in the order in which they appear in the list. Optionally, a constant offset can be applied to each index.
+ *
+ * The algorithm will translate the list of indices to a sequence of multiple vertex loading and
+ * triangle drawing commands. This translation is optimized for efficient loading, as opposed to run time
+ * of the algorithm itself. Therefore it is advised to record the generated commands into a block whenever possible,
+ * to avoid the translation to be reduntantly computed on every frame, even though the result will always be the same.
+ *
+ * This is a drawing command and must be placed between a pair of calls to #mg_draw_begin and #mg_draw_end.
+ *
+ * Can be recorded into blocks.
+ *
+ * @param[in] input_assembly_parms Pointer to a struct containing parameters for input assembly.
+ * @param[in] indices Pointer to the list of indices.
+ * @param[in] index_count The number of indices that should be read.
+ * @param[in] vertex_offset A constant offset that will be added to all index values.
+ *
+ * @see #mg_input_assembly_parms_t
+ * @see #mg_draw_begin
+ * @see #mg_draw_end
+ */
+LIBDRAGON_PREVIEW_API
+void mg_draw_indexed(const mg_input_assembly_parms_t *input_assembly_parms, const uint16_t *indices, uint32_t index_count, int32_t vertex_offset);
+
+/// @cond
+
+/* Inline function definitions and prerequisites */
+
+#define mg_cmd_write(cmd_id, ...) rspq_write(mg_overlay_id, cmd_id, ##__VA_ARGS__)
+#define mg_rdpq_write(cmd_id, ...) rdpq_write(-1, mg_overlay_id, cmd_id, ##__VA_ARGS__)
+
+extern uint32_t mg_overlay_id;
+
+enum
+{
+ MG_CMD_SET_BYTE = 0x0,
+ MG_CMD_SET_SHORT = 0x1,
+ MG_CMD_SET_WORD = 0x2,
+ MG_CMD_SET_QUAD = 0x3,
+ MG_CMD_SET_SHADER = 0x4,
+ MG_CMD_LOAD_VERTICES = 0x5,
+ MG_CMD_DRAW_INDICES = 0x6,
+ MG_CMD_DRAW_END = 0x7,
+ MG_CMD_LOAD_UNIFORM = 0x8,
+ MG_CMD_INLINE_UNIFORM_8 = 0x9,
+ MG_CMD_INLINE_UNIFORM_16 = 0xA,
+ MG_CMD_INLINE_UNIFORM_32 = 0xB,
+ MG_CMD_INLINE_UNIFORM_64 = 0xC,
+ MG_CMD_INLINE_UNIFORM_128 = 0xD,
+ MG_CMD_INLINE_UNIFORM_MAX = 0xE,
+};
+
+typedef struct
+{
+ uint16_t scale[4];
+ uint16_t offset[4];
+} mg_rsp_viewport_t;
+
+typedef struct
+{
+ mg_rsp_viewport_t viewport;
+ uint16_t clip_factors[4];
+ int16_t vertex_size[4];
+ uint32_t shader_code;
+ uint32_t shader_code_size;
+ uint32_t vertex_buffer;
+ uint16_t tri_cmd;
+ uint8_t cull_mode;
+ uint8_t output_offset;
+} __attribute__((packed)) mg_rsp_state_t;
+
+LIBDRAGON_PREVIEW_API
+inline void mg_cmd_set_byte(uint32_t offset, uint8_t value)
+{
+ mg_cmd_write(MG_CMD_SET_BYTE, offset, value);
+}
+
+LIBDRAGON_PREVIEW_API
+inline void mg_cmd_set_short(uint32_t offset, uint16_t value)
+{
+ mg_cmd_write(MG_CMD_SET_SHORT, offset, value);
+}
+
+LIBDRAGON_PREVIEW_API
+inline void mg_cmd_set_word(uint32_t offset, uint32_t value)
+{
+ mg_cmd_write(MG_CMD_SET_WORD, offset, value);
+}
+
+LIBDRAGON_PREVIEW_API
+inline void mg_cmd_set_quad(uint32_t offset, uint32_t value0, uint32_t value1, uint32_t value2, uint32_t value3)
+{
+ mg_cmd_write(MG_CMD_SET_QUAD, offset, value0, value1, value2, value3);
+}
+
+LIBDRAGON_PREVIEW_API
+inline uint8_t mg_culling_parms_to_rsp_state(const mg_culling_parms_t *culling)
+{
+ uint8_t cull_mode;
+ uint8_t is_front_cw;
+
+ switch (culling->cull_mode) {
+ case MG_CULL_MODE_NONE:
+ cull_mode = 2;
+ break;
+ case MG_CULL_MODE_BACK:
+ cull_mode = 1;
+ break;
+ case MG_CULL_MODE_FRONT:
+ cull_mode = 0;
+ break;
+ default:
+ assertf(0, "%d is not a valid cull mode!", culling->cull_mode);
+ cull_mode = 2;
+ }
+
+ switch (culling->front_face) {
+ case MG_FRONT_FACE_COUNTER_CLOCKWISE:
+ is_front_cw = 0;
+ break;
+ case MG_FRONT_FACE_CLOCKWISE:
+ is_front_cw = 1;
+ break;
+ default:
+ assertf(0, "%d is not a valid front face winding direction!", culling->front_face);
+ is_front_cw = 0;
+ }
+
+ // If the front face is clockwise, flip the cull mode.
+ // If the cull mode is NONE anyway, this has no effect because the final value is still > 1
+ return cull_mode ^ is_front_cw;
+}
+
+LIBDRAGON_PREVIEW_API
+inline void mg_set_culling(const mg_culling_parms_t *culling)
+{
+ mg_cmd_set_byte(offsetof(mg_rsp_state_t, cull_mode), mg_culling_parms_to_rsp_state(culling));
+}
+
+LIBDRAGON_PREVIEW_API
+inline void mg_set_geometry_flags(mg_geometry_flags_t flags)
+{
+ uint16_t tricmd = 0x8 | (flags&0x7);
+ mg_cmd_set_short(offsetof(mg_rsp_state_t, tri_cmd), tricmd << 8);
+}
+
+LIBDRAGON_PREVIEW_API
+inline void mg_set_viewport_fullscreen(resolution_t res)
+{
+ mg_viewport_t viewport = {
+ .x = 0,
+ .y = 0,
+ .width = (float) res.width,
+ .height = (float) res.height,
+ .minDepth = 0.0f,
+ .maxDepth = 1.0f,
+ };
+ mg_set_viewport(&viewport);
+}
+
+LIBDRAGON_PREVIEW_API
+inline void mg_set_clip_factor(uint32_t factor)
+{
+ mg_cmd_set_word(offsetof(mg_rsp_state_t, clip_factors) + sizeof(uint16_t)*2, (factor<<16) | factor);
+}
+
+LIBDRAGON_PREVIEW_API
+inline void mg_uniform_load(const mg_uniform_t *uniform, const void *data)
+{
+ mg_uniform_load_raw(uniform->offset, uniform->size, data);
+}
+
+LIBDRAGON_PREVIEW_API
+inline void mg_uniform_load_inline(const mg_uniform_t *uniform, const void *data)
+{
+ mg_uniform_load_inline_raw(uniform->offset, uniform->size, data);
+}
+
+LIBDRAGON_PREVIEW_API
+inline void mg_bind_vertex_buffer(const void *buffer)
+{
+ mg_cmd_set_word(offsetof(mg_rsp_state_t, vertex_buffer), PhysicalAddr(buffer));
+}
+/// @endcond
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/magma_constants.h b/include/magma_constants.h
new file mode 100644
index 000000000..d224d0f1e
--- /dev/null
+++ b/include/magma_constants.h
@@ -0,0 +1,52 @@
+/**
+ * @file magma_constants.h
+ * @author Dennis Heinze
+ * @brief Constant definitions for magma
+ * @ingroup magma
+ */
+
+#ifndef __MAGMA_CONSTANTS
+#define __MAGMA_CONSTANTS
+
+/**
+ * @brief The maximum number of vertices that the vertex cache can hold.
+ *
+ * @see #mg_load_vertices
+ * @see #mg_draw_triangle
+ */
+#define MG_VERTEX_CACHE_COUNT 48
+
+/** @brief Default value of the clipping guard factor.
+ *
+ * @see #mg_set_clip_factor
+ */
+#define MG_DEFAULT_GUARD_BAND 2
+
+/// @cond
+
+#define MG_INLINE_UNIFORM_HEADER 12
+#define MG_MAX_UNIFORM_PAYLOAD_SIZE (RSPQ_MAX_COMMAND_SIZE*4 - MG_INLINE_UNIFORM_HEADER)
+
+#define MG_CLIP_PLANE_COUNT 6
+#define MG_CLIP_PLANE_SIZE 8
+#define MG_CLIP_CACHE_SIZE 9
+
+#define MG_VTX_XYZ 0
+#define MG_VTX_CLIP_CODE 6
+#define MG_VTX_TR_CODE 7
+#define MG_VTX_RGBA 8
+#define MG_VTX_ST 12
+#define MG_VTX_Wi 16
+#define MG_VTX_Wf 18
+#define MG_VTX_INVWi 20
+#define MG_VTX_INVWf 22
+#define MG_VTX_CS_POSi 24
+#define MG_VTX_CS_POSf 32
+#define MG_VTX_SIZE 40
+#define MG_VTX_SIZE2 80
+
+#define MG_MAX_UNIFORMS_SIZE 0x200
+
+/// @endcond
+
+#endif
diff --git a/include/magma_math.h b/include/magma_math.h
new file mode 100644
index 000000000..ace1c8d87
--- /dev/null
+++ b/include/magma_math.h
@@ -0,0 +1,42 @@
+/**
+ * @file magma_math.h
+ * @author Dennis Heinze
+ * @brief Math helper functions for magma
+ * @ingroup magma
+ */
+
+#ifndef __MAGMA_MATH
+#define __MAGMA_MATH
+
+#include "preview.h"
+#include "fgeom.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Create a magma-compatible perspective projection matrix.
+ * @preview
+ *
+ * The resulting view space coordinate system is right handed, with positive X pointing right,
+ * positive Y pointing up, and positive Z pointing towards the camera (out of the screen).
+ */
+LIBDRAGON_PREVIEW_API
+void mg_mat4_perspective(fm_mat4_t *out, float fovy, float aspect, float z_near, float z_far);
+
+/**
+ * @brief Create a magma-compatible orthographic projection matrix.
+ * @preview
+ *
+ * The resulting view space coordinate system is right handed, with positive X pointing right,
+ * positive Y pointing up, and positive Z pointing towards the camera (out of the screen).
+ */
+LIBDRAGON_PREVIEW_API
+void mg_mat4_ortho(fm_mat4_t *out, float l, float r, float b, float t, float n, float f);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/magma_types.h b/include/magma_types.h
new file mode 100644
index 000000000..84ddeb98e
--- /dev/null
+++ b/include/magma_types.h
@@ -0,0 +1,285 @@
+/**
+ * @file magma_types.h
+ * @author Dennis Heinze
+ * @brief Magma type definitions
+ * @ingroup magma
+ */
+
+#ifndef __LIBDRAGON_MAGMA_TYPES_H
+#define __LIBDRAGON_MAGMA_TYPES_H
+
+#include
+#include "rsp.h"
+
+/**
+ * @brief An instance of a vertex shader that has been configured with a specific vertex layout.
+ *
+ * @see #mg_pipeline_create
+ * @see #mg_pipeline_bind
+ */
+typedef struct mg_pipeline_s mg_pipeline_t;
+
+/**
+ * @brief Vertex attribute descriptor.
+ *
+ * @see #mg_vertex_layout_t
+ */
+typedef struct
+{
+ uint32_t input; ///< The input number of this attribute.
+ uint32_t offset; ///< The offset in bytes of this attribute relative to the start of a vertex.
+} mg_vertex_attribute_t;
+
+/**
+ * @brief Configuration of a pipeline's vertex layout.
+ *
+ * This configuration specifies how the data from a vertex buffer should be fed into the vertex shader.
+ *
+ * A vertex shader defines the set of vertex inputs it supports. A vertex input is defined by:
+ * * Its "input number", which is a unique identifier
+ * * An alignment requirement
+ * * Whether it is optional or not
+ *
+ * The vertex layout of a pipeline is defined by its overall stride (the distance between consecutive vertices in the buffer),
+ * and a collection of vertex attributes. Each attribute is defined by:
+ * * Its offset relative from the start of a vertex
+ * * An input number, which creates a mapping to some vertex input
+ *
+ * To create a valid pipeline, its vertex layout must be compatible with the vertex inputs defined by the vertex shader.
+ * A vertex layout is compatible with a shader if, and only if, all of the following requirements are met:
+ * * All of its vertex attributes have an input number that is present in the shader
+ * * There are no duplicate input numbers among the vertex attributes
+ * * For each non-optional vertex input in the shader, there is a vertex attribute with the respective input number.
+ * * All vertex attributes satisfy the alignment requirements of their respective vertex inputs. This is the case
+ * if, and only if, the vertex attribute's offset is a multiple of the required alignment.
+ *
+ * The above implies that optional vertex inputs may be omitted from the vertex layout.
+ * However, there is no further guarantee about the behavior of the shader in case of omitted inputs.
+ *
+ * Also note that, besides alignment, there are no further requirements on the offsets of each vertex attribute.
+ * This means they don't need to be packed tightly and may even overlap.
+ * How many bytes are actually read per vertex input is defined by the vertex shader.
+ *
+ * @see #mg_pipeline_parms_t
+ * @see #mg_pipeline_create
+ */
+typedef struct
+{
+ uint32_t stride; ///< The distance in bytes between two consecutive vertices.
+ uint32_t attribute_count; ///< The number of vertex attribute descriptors.
+ mg_vertex_attribute_t *attributes; ///< Pointer to the array of vertex attribute descriptors.
+} mg_vertex_layout_t;
+
+/**
+ * @brief Parameters for #mg_pipeline_create.
+ */
+typedef struct
+{
+ /**
+ * @brief The ucode from which to create the pipeline.
+ *
+ * This ucode must be compatible with being a magma vertex shader. In other words, it must adhere to a certain
+ * contract that is defined by magma. The exact details of this are too complex to list here. In general,
+ * the ucode should include the file `rsp_magma.inc` at the top and call some special macros to define the shader.
+ * See that file for more details.
+ */
+ rsp_ucode_t *vertex_shader_ucode;
+ mg_vertex_layout_t vertex_layout; ///< Vertex layout configuration.
+} mg_pipeline_parms_t;
+
+/**
+ * @brief Uniform descriptor.
+ *
+ * A uniform is a piece of memory that can contain some input for a vertex shader which does not change per vertex.
+ * More specifically, a uniform defines some region of memory that is reserved by a pipeline, which the pipeline's
+ * vertex shader can access during its runtime. The combined reserved memory for all uniforms in a pipeline is known as
+ * the pipeline's "uniform memory". The uniform memory can be thought of as some virtual address space which starts
+ * at 0 and goes up to however many bytes a pipeline's uniforms take up in total.
+ *
+ * A uniform is defined by:
+ * * Its starting offset in uniform memory
+ * * Its size
+ * * An identifier which is called the binding number.
+ *
+ * The binding number must only be unique for each uniform within a pipeline. Otherwise, binding numbers can take on arbitrary
+ * values and must not even be consecutive or otherwise correlated with a uniform's location in uniform memory.
+ *
+ * Uniforms are defined by the vertex shader that is referenced by a pipeline. All pipelines that reference the same
+ * shader will contain equivalent sets of uniforms. Uniforms can be queried from a pipeline using #mg_pipeline_get_uniform.
+ *
+ * To provide inputs to a vertex shader via uniforms, they must be loaded first using one of the uniform loading functions,
+ * for example #mg_uniform_load or #mg_uniform_load_inline. Those functions will load the entirety of a uniform's memory at once.
+ * It is also possible to load values in a more advanced manner using #mg_uniform_load_raw and #mg_uniform_load_inline_raw.
+ *
+ * @see #mg_pipeline_get_uniform
+ * @see #mg_uniform_load
+ * @see #mg_uniform_load_inline
+ * @see #mg_uniform_load_raw
+ * @see #mg_uniform_load_inline_raw
+ */
+typedef struct
+{
+ uint32_t binding; ///< The uniform's binding number.
+ uint32_t offset; ///< The offset in bytes where this uniform is located, from the start of the pipeline's uniform memory.
+ uint32_t size; ///< The uniform's size in bytes.
+} mg_uniform_t;
+
+/**
+ * @brief The set of bit flags that can be passed to #mg_set_geometry_flags.
+ *
+ * These flags configure which geometry attributes will be passed to the hardware rasterizer
+ * when drawing triangles. This is relevant in combination with #rdpq_mode_combiner and/or #rdpq_mode_zbuf.
+ *
+ * @see #mg_set_geometry_flags
+ */
+typedef enum
+{
+ /**
+ * @brief If set, Z values will be transmitted to the rasterizer.
+ *
+ * Should be set if depth comparison or Z update were enabled in #rdpq_mode_zbuf.
+ */
+ MG_GEOMETRY_FLAGS_Z_ENABLED = 1<<0,
+
+ /**
+ * @brief If set, texture coordinates will be transmitted to the rasterizer.
+ *
+ * Should be set if #rdpq_mode_combiner was configured with any texture input.
+ */
+ MG_GEOMETRY_FLAGS_TEX_ENABLED = 1<<1,
+
+ /**
+ * @brief If set, shade values will be transmitted to the rasterizer.
+ *
+ * Should be set if #rdpq_mode_combiner was configured with any shade input.
+ */
+ MG_GEOMETRY_FLAGS_SHADE_ENABLED = 1<<2,
+} mg_geometry_flags_t;
+
+/**
+ * @brief Enumeration of possible face culling modes.
+ *
+ * @see #mg_culling_parms_t
+ * @see #mg_set_culling
+ */
+typedef enum
+{
+ MG_CULL_MODE_NONE = 0, ///< No faces will be culled.
+ MG_CULL_MODE_BACK = 1, ///< Back faces will be culled.
+ MG_CULL_MODE_FRONT = 2, ///< Front faces will be culled.
+} mg_cull_mode_t;
+
+/**
+ * @brief Enumeration of possible values for front face configuration.
+ *
+ * @see #mg_culling_parms_t
+ * @see #mg_set_culling
+ */
+typedef enum
+{
+ MG_FRONT_FACE_COUNTER_CLOCKWISE = 0, ///< Faces with counter clockwise winding direction are defined as front faces.
+ MG_FRONT_FACE_CLOCKWISE = 1, ///< Faces with clockwise winding direction are defined as front faces.
+} mg_front_face_t;
+
+/**
+ * @brief Parameters for #mg_set_culling.
+ *
+ * @see #mg_set_culling
+ */
+typedef struct
+{
+ mg_cull_mode_t cull_mode; ///< Specifies which faces should be culled.
+ mg_front_face_t front_face; ///< Defines the winding direction of front faces.
+} mg_culling_parms_t;
+
+/**
+ * @brief Description of the target area in the framebuffer that will be drawn to.
+ *
+ * @see #mg_set_viewport
+ */
+typedef struct
+{
+ float x; ///< X-coordinate of the viewport's upper left corner in pixels.
+ float y; ///< Y-coordinate of the viewport's upper left corner in pixels.
+ float width; ///< Width of the viewport in pixels.
+ float height; ///< Height of the viewport in pixels.
+ float minDepth; ///< Lower end of the viewport's depth range.
+ float maxDepth; ///< Higher end of the viewport's depth range.
+ float z_near; ///< Distance of the near clipping plane from the camera. Used for perspective normalization.
+ float z_far; ///< Distance of the far clipping plane from the camera. Used for perspective normalization.
+} mg_viewport_t;
+
+/**
+ * @brief Enumeration of possible primitive construction modes.
+ *
+ * @see #mg_input_assembly_parms_t
+ * @see #mg_draw
+ * @see #mg_draw_indexed
+ */
+typedef enum
+{
+ /**
+ * @brief Separate triangles are constructed for every 3 indices in the list.
+ *
+ * Triangles are defined the following equation, where `t{n}` is the nth triangle, and `v{n}` is the nth vertex:
+ * @code
+ * t{i} = (v{3i}, v{3i+1}, v{3i+2})
+ * @endcode
+ *
+ * For a list of `n` indices, `floor(n/3)` triangles will be constructed.
+ */
+ MG_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST = 0,
+
+ /**
+ * @brief Connected triangles are constructed with consecutive triangles sharing an edge.
+ *
+ * Triangles are defined the following equation, where `t{n}` is the nth triangle, and `v{n}` is the nth vertex:
+ * @code
+ * t{i} = (v{i}, v{i+(1+i%2)}, v{i+(2-i%2)})
+ * @endcode
+ *
+ * For a list of `n` indices, `max(0,n-2)` triangles will be constructed.
+ */
+ MG_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP = 1,
+
+ /**
+ * @brief Connected triangles are constructed with all triangles sharing a common vertex.
+ *
+ * Triangles are defined the following equation, where `t{n}` is the nth triangle, and `v{n}` is the nth vertex:
+ * @code
+ * t{i} = (v{i+1}, v{i+2}, v{0})
+ * @endcode
+ *
+ * For a list of `n` indices, `max(0,n-2)` triangles will be constructed.
+ */
+ MG_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN = 2,
+} mg_primitive_topology_t;
+
+/**
+ * @brief Describes how primitives are assembled from a list of indices.
+ *
+ * Set #mtx_indices to a non-NULL pointer to enable skinning via matrix indices.
+ * If enabled, each vertex will be assigned a unsigned 8-bit matrix index.
+ * These indices are read in order from #mtx_indices with a stride of #mtx_indices_stride.
+ * Spans of consecutive vertices with the same index will be read as batches.
+ * Before each batch, the uniform specified by #matrix_uniform will automatically be loaded.
+ * The pointer to load the value from is obtained as follows: `matrices + index * matrices_stride`.
+ *
+ * NOTE: The uniform is *not* loaded inline. This means that the contents of #matrices need to remain
+ * valid until any actual loads have been performed (see #mg_uniform_load).
+ *
+ * @see #mg_draw
+ * @see #mg_draw_indexed
+ */
+typedef struct
+{
+ mg_primitive_topology_t primitive_topology; ///< The topology mode of the constructed primitives.
+ bool primitive_restart_enabled; ///< If true, construction of primitives will restart whenever a special index value (-1) is encountered in the list.
+ const void *mtx_indices; ///< Pointer to memory where unsigned 8-bit matrix indices are stored, or NULL if matrix indices are disabled.
+ uint32_t mtx_indices_stride; ///< Number of bytes to advance to get to the next matrix index.
+ const void *matrices; ///< Pointer to memory where matrix data is stored.
+ uint32_t matrices_stride; ///< Number of bytes to advance to get to the next matrix.
+ mg_uniform_t matrix_uniform; ///< Uniform that is used to load matrix data.
+} mg_input_assembly_parms_t;
+
+#endif
diff --git a/include/mempak.h b/include/mempak.h
index 43af2762c..0e960a27e 100644
--- a/include/mempak.h
+++ b/include/mempak.h
@@ -4,6 +4,18 @@
* @author thekovic
* @brief Controller Pak Filesystem Routines
* @ingroup controllerpak
+ *
+ * @deprecated This module is deprecated. Please use the APIs cpak.h instead.
+ *
+ * To read and write to the Controller Pak in an organized way compatible with official software,
+ * first check that the Controller Pak is valid using #validate_mempak. If the Controller Pak is
+ * invalid, it will need to be formatted using #format_mempak. Once the Controller Pak is
+ * considered valid, existing notes can be enumerated using #get_mempak_entry. To
+ * read the data associated with a note, use #read_mempak_entry_data. To write a
+ * new note to the Controller Pak, use #write_mempak_entry_data. Note that there is no append
+ * functionality so if a note is being updated, ensure you have deleted the old note
+ * first using #delete_mempak_entry. Code should be careful to check how many blocks
+ * are free before writing using #get_mempak_free_space.
*/
#ifndef __LIBDRAGON_MEMPAK_H
#define __LIBDRAGON_MEMPAK_H
@@ -17,16 +29,9 @@
* read from or write to a Controller Pak, be sure you have initialized the Joypad subsystem
* with #joypad_init and verified that you have a Controller Pak in the correct controller
* using #joypad_get_accessory_type.
+ *
+ * See cpak.h for the API.
*
- * To read and write to the Controller Pak in an organized way compatible with official software,
- * first check that the Controller Pak is valid using #validate_mempak. If the Controller Pak is
- * invalid, it will need to be formatted using #format_mempak. Once the Controller Pak is
- * considered valid, existing notes can be enumerated using #get_mempak_entry. To
- * read the data associated with a note, use #read_mempak_entry_data. To write a
- * new note to the Controller Pak, use #write_mempak_entry_data. Note that there is no append
- * functionality so if a note is being updated, ensure you have deleted the old note
- * first using #delete_mempak_entry. Code should be careful to check how many blocks
- * are free before writing using #get_mempak_free_space.
*
* @{
*/
@@ -55,18 +60,28 @@ typedef struct entry_structure
/** @brief ID of this entry */
uint8_t entry_id;
/**
- * @brief Name of this entry
- *
- * The complete list of valid ASCII characters in a note name is:
+ * @brief Name of this entry (UTF-8)
*
+ * The name is limited to 16 characters for name, and 4 characters for
+ * extension (though most games only show the first character of the
+ * extension). The extension is separated from the name using a dot.
+ * The valid characters set is very limited, and contains
+ * only a subset of ASCII or Katakana. The complete character map is:
+ *
*
- * ABCDEFGHIJKLMNOPQRSTUVWXYZ!"#`*+,-./:=?\@
+ * 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ!"#'*+,-./:=?@
+ * 。゛゜ァィゥェォッャュョヲンアイウエオカキクケコサシスセソタ
+ * チツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワガギグゲ
+ * ゴザジズゼゾダヂヅデドバビブベボパピプペポ
*
*
* The space character is also allowed. Any other character will be
* converted to a space before writing to the Controller Pak.
+ *
+ * The buffer here is longer because it allows for UTF-8 encoding of the
+ * Japanese characters.
*/
- char name[19];
+ char name[16*3 + 1 + 4*3 + 1];
} entry_structure_t;
#ifdef __cplusplus
@@ -90,6 +105,7 @@ extern "C" {
* @retval -1 if the sector was out of bounds or sector_data was null
* @retval -2 if there was an error reading part of a sector
*/
+__attribute__((deprecated("use cpak_read instead")))
int read_mempak_sector( int controller, int sector, uint8_t *sector_data );
/**
@@ -109,6 +125,7 @@ int read_mempak_sector( int controller, int sector, uint8_t *sector_data );
* @retval -1 if the sector was out of bounds or sector_data was null
* @retval -2 if there was an error writing part of a sector
*/
+__attribute__((deprecated("use cpak_write instead")))
int write_mempak_sector( int controller, int sector, uint8_t *sector_data );
/**
@@ -124,6 +141,7 @@ int write_mempak_sector( int controller, int sector, uint8_t *sector_data );
* @retval -2 if the Controller Pak is not present or couldn't be read
* @retval -3 if the Controller Pak is bad or unformatted
*/
+__attribute__((deprecated("use cpakfs_fsck() instead")))
int validate_mempak( int controller );
/**
@@ -137,6 +155,7 @@ int validate_mempak( int controller );
*
* @return The number of blocks free on the memory card or a negative number on failure
*/
+__attribute__((deprecated("use cpakfs_get_stats() instead")))
int get_mempak_free_space( int controller );
/**
@@ -156,6 +175,7 @@ int get_mempak_free_space( int controller );
* @retval -1 if the entry is out of bounds or entry_data is null
* @retval -2 if the Controller Pak is bad or not present
*/
+__attribute__((deprecated("use the new cpak filesystem (cpak.h) instead")))
int get_mempak_entry( int controller, int entry, entry_structure_t *entry_data );
/**
@@ -170,6 +190,7 @@ int get_mempak_entry( int controller, int entry, entry_structure_t *entry_data )
* @retval 0 if the Controller Pak was formatted successfully.
* @retval -2 if the Controller Pak was not present or couldn't be formatted.
*/
+__attribute__((deprecated("use cpakfs_format() instead")))
int format_mempak( int controller );
/**
@@ -194,6 +215,7 @@ int format_mempak( int controller );
* @retval -2 if the Controller Pak was not present or bad
* @retval -3 if the data couldn't be read
*/
+__attribute__((deprecated("use the new cpak filesystem (cpak.h) instead")))
int read_mempak_entry_data( int controller, entry_structure_t *entry, uint8_t *data );
/**
@@ -218,6 +240,7 @@ int read_mempak_entry_data( int controller, entry_structure_t *entry, uint8_t *d
* @retval -4 if there wasn't enough space to store the note
* @retval -5 if there is no room in the TOC to add a new entry
*/
+__attribute__((deprecated("use the new cpak filesystem (cpak.h) instead")))
int write_mempak_entry_data( int controller, entry_structure_t *entry, uint8_t *data );
/**
@@ -235,6 +258,7 @@ int write_mempak_entry_data( int controller, entry_structure_t *entry, uint8_t *
* @retval -1 if the entry was invalid
* @retval -2 if the Controller Pak was bad or not present
*/
+__attribute__((deprecated("use the new cpak filesystem (cpak.h) instead")))
int delete_mempak_entry( int controller, entry_structure_t *entry );
#ifdef __cplusplus
diff --git a/include/mgfx.h b/include/mgfx.h
new file mode 100644
index 000000000..af461670f
--- /dev/null
+++ b/include/mgfx.h
@@ -0,0 +1,202 @@
+/**
+ * @file mgfx.h
+ * @author Dennis Heinze
+ * @brief Interface for magma's "fixed function" builtin shader
+ * @ingroup magma
+ */
+
+#ifndef __LIBDRAGON_MGFX_H
+#define __LIBDRAGON_MGFX_H
+
+
+#include "preview.h"
+#include "magma.h"
+#include "mgfx_constants.h"
+#include "mgfx_macros.h"
+#include "fgeom.h"
+
+/** @brief Flagged enum of features that can be optionally enabled when creating a mgfx pipeline. */
+typedef enum
+{
+ MGFX_FEATURE_ENV_MAP = (1<<0) ///< Spherical environment mapping.
+} mgfx_features_t;
+
+/** @brief Data structure of a single matrix in the matrices uniform. */
+typedef struct
+{
+ int16_t i[16]; ///< Integer parts of the matrix coefficients.
+ uint16_t f[16]; ///< Fractional parts of the matrix coefficients.
+} __attribute__((packed, aligned(16))) mgfx_matrix_t;
+
+_Static_assert(sizeof(mgfx_matrix_t) == MGFX_MATRIX_SIZE, "Matrix struct size is incorrect!");
+
+/** @brief Data structure of the matrices uniform. */
+typedef struct
+{
+ mgfx_matrix_t mvp; ///< The model-view-projection matrix.
+ mgfx_matrix_t mv; ///< The model-view matrix.
+ int16_t normal[16]; ///< The normal matrix.
+} __attribute__((packed, aligned(16))) mgfx_matrices_t;
+
+/** @brief Data structure of the texturing uniform. */
+typedef struct
+{
+ int16_t tex_scale[2]; ///< Texture coordinate scale.
+ int16_t tex_offset[2]; ///< Texture coordinate offset.
+} __attribute__((packed, aligned(16))) mgfx_texturing_t;
+
+/** @brief Data structure of a single light in the lighting uniform. */
+typedef struct
+{
+ int16_t position[4]; ///< The light's position.
+ int16_t color[3]; ///< The light's color.
+ int16_t intensity; ///< The light's intensity.
+} __attribute__((packed, aligned(16))) mgfx_light_t;
+
+_Static_assert(offsetof(mgfx_light_t, position) == MGFX_LIGHT_POSITION, "Light position is at an incorrect offset!");
+_Static_assert(offsetof(mgfx_light_t, color) == MGFX_LIGHT_COLOR, "Light color is at an incorrect offset!");
+_Static_assert(offsetof(mgfx_light_t, intensity) == MGFX_LIGHT_INTENSITY, "Light intensity is at an incorrect offset!");
+_Static_assert(sizeof(mgfx_light_t) == MGFX_LIGHT_SIZE, "Light struct size is incorrect!");
+
+/** @brief Data structure of the lighting uniform. */
+typedef struct
+{
+ mgfx_light_t lights[MGFX_LIGHT_COUNT_MAX]; ///< Array of lights.
+ int16_t ambient[3]; ///< The ambient color.
+ uint8_t has_points; ///< Non-zero if there are any point lights.
+ uint8_t count; ///< Number of lights.
+} __attribute__((packed, aligned(16))) mgfx_lighting_t;
+
+/** @brief Data structure of the fog uniform. */
+typedef struct
+{
+ int16_t offset_int; ///< Integer part of the fog offset.
+ uint16_t offset_frac; ///< Fractional part of the fog offset.
+ int16_t factor_int; ///< Integer part of the fog factor.
+ uint8_t mask; ///< Mask that determines whether the fog factor is loaded into vertex alpha.
+} __attribute__((packed, aligned(16))) mgfx_fog_t;
+
+/** @brief Matrices parameters. */
+typedef struct
+{
+ float *model_view_projection; ///< The model-view-projection matrix in column-major order.
+ float *model_view; ///< The model-view matrix in column-major order.
+ float *normal; ///< The normal matrix in column-major order.
+} mgfx_matrices_parms_t;
+
+/** @brief Texturing parameters. */
+typedef struct
+{
+ int16_t scale[2]; ///< Factors by which texture coordinates are be scaled.
+ int16_t offset[2]; ///< Offsets that are added to texture coordinates after scaling.
+} mgfx_texturing_parms_t;
+
+/** @brief Parameters of a single light. */
+typedef struct
+{
+ /**
+ * @brief The light's position/direction.
+ *
+ * If w is 0, the light is directional. In this case x, y and z specify the light's (normalized) direction in view space.
+ * Otherwise, the light is positional. In this case x, y and z specify the light's position in view space.
+ */
+ fm_vec4_t position;
+
+ color_t color; ///< The light's color.
+ float intensity; ///< The light's intensity. Must only be set if the light is positional.
+} mgfx_light_parms_t;
+
+/** @brief Lighting parameters. */
+typedef struct
+{
+ color_t ambient_color; ///< The ambient light color, which is always added to all geometry.
+ mgfx_light_parms_t *lights; ///< The list of lights.
+ uint32_t light_count; ///< The number of lights in the list. The maximum value is #MGFX_LIGHT_COUNT_MAX.
+} mgfx_lighting_parms_t;
+
+/**
+ * @brief Fog parameters.
+ *
+ * Fog is disabled when start == end.
+ * When fog is enabled, vertex alpha is always overwritten by the computed fog factor.
+ * Note that fog must also be enabled in rdpq to work properly (see #rdpq_mode_fog).
+ */
+typedef struct
+{
+ float start; ///< Distance from the eye position where fog starts. Geometry closer than this distance will receive no fog at all.
+ float end; ///< Distance from the eye position where fog ends. Geometry farther away than this distance will be fully fog-colored.
+} mgfx_fog_parms_t;
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Returns a pointer to the mgfx shader ucode. Use this to create a pipeline with this shader.
+ * @preview
+ *
+ * @see #mg_pipeline_create
+ */
+LIBDRAGON_PREVIEW_API
+inline rsp_ucode_t *mgfx_get_shader_ucode(mgfx_features_t features)
+{
+ extern rsp_ucode_t rsp_mgfx;
+ extern rsp_ucode_t rsp_mgfx_env;
+
+ if (features & MGFX_FEATURE_ENV_MAP) return &rsp_mgfx_env;
+ return &rsp_mgfx;
+}
+
+/** @brief Convert matrices parameters to the internal data structure of the matrices uniform.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+void mgfx_get_matrices(mgfx_matrices_t *dst, const mgfx_matrices_parms_t *parms);
+
+/** @brief Convert texturing parameters to the internal data structure of the texturing uniform.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+void mgfx_get_texturing(mgfx_texturing_t *dst, const mgfx_texturing_parms_t *parms);
+
+/** @brief Convert lighting parameters to the internal data structure of the lighting uniform.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+void mgfx_get_lighting(mgfx_lighting_t *dst, const mgfx_lighting_parms_t *parms);
+
+/** @brief Convert fog parameters to the internal data structure of the fog uniform.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+void mgfx_get_fog(mgfx_fog_t *dst, const mgfx_fog_parms_t *parms);
+
+/** @brief Set the value of the matrices uniform inline.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+void mgfx_set_matrices_inline(const mg_uniform_t *uniform, const mgfx_matrices_parms_t *parms);
+
+/** @brief Set the value of the texturing uniform inline.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+void mgfx_set_texturing_inline(const mg_uniform_t *uniform, const mgfx_texturing_parms_t *parms);
+
+/** @brief Set the value of the lighting uniform inline.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+void mgfx_set_lighting_inline(const mg_uniform_t *uniform, const mgfx_lighting_parms_t *parms);
+
+/** @brief Set the value of the fog uniform inline.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+void mgfx_set_fog_inline(const mg_uniform_t *uniform, const mgfx_fog_parms_t *parms);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/mgfx_constants.h b/include/mgfx_constants.h
new file mode 100644
index 000000000..425e7b1ba
--- /dev/null
+++ b/include/mgfx_constants.h
@@ -0,0 +1,119 @@
+/**
+ * @file mgfx_constants.h
+ * @author Dennis Heinze
+ * @brief Constant definitions for mgfx
+ * @ingroup magma
+ */
+
+#ifndef __MGFX_CONSTANTS
+#define __MGFX_CONSTANTS
+
+/**
+ * @brief Input number of the "Position" vertex input.
+ *
+ * This attribute expects the object space position as three 16-bit signed integers in s10.5 format.
+ * It is equivalent to:
+ *
+ * @code{.c}
+ * struct position {
+ * int16_t x;
+ * int16_t y;
+ * int16_t z;
+ * }
+ * @endcode
+ */
+#define MGFX_ATTRIBUTE_POSITION 0
+
+/**
+ * @brief Input number of the "Normal" vertex input.
+ *
+ * This attribute expects the object space normal as three normalized signed integers (in the range -1 to 1) packed into 16 bits.
+ * The x, y and z components are encoded as 5, 6 and 5 bits each, like so:
+ * |16----------------------------0|
+ * |x x x x x y y y y y y z z z z z|
+ */
+#define MGFX_ATTRIBUTE_NORMAL 1
+
+/**
+ * @brief Input number of the "Color" vertex input.
+ *
+ * This attribute expects the RGBA color as four normalized 8-bit unsigned integers (in the range 0 to 1).
+ * It is equivalent to:
+ *
+ * @code{.c}
+ * struct color {
+ * uint8_t r;
+ * uint8_t g;
+ * uint8_t b;
+ * uint8_t a;
+ * }
+ * @endcode
+ */
+#define MGFX_ATTRIBUTE_COLOR 2
+
+/**
+ * @brief Input number of the "Texture coordinate" vertex input.
+ *
+ * This attribute expects the texture coordinates as two 16-bit signed integers.
+ * The scale and offset of the texture coordinates can be adjusted in #mgfx_texturing_parms_t.
+ * The format is equivalent to:
+ *
+ * @code{.c}
+ * struct texcoord {
+ * int16_t s;
+ * int16_t t;
+ * }
+ * @endcode
+ */
+#define MGFX_ATTRIBUTE_TEXCOORD 3
+
+/**
+ * @brief Binding number of the "Matrices" uniform.
+ *
+ * @see #mgfx_get_matrices
+ * @see #mgfx_set_matrices_inline
+ */
+#define MGFX_BINDING_MATRICES 0
+
+/**
+ * @brief Binding number of the "Texturing" uniform.
+ *
+ * @see #mgfx_get_texturing
+ * @see #mgfx_set_texturing_inline
+ */
+#define MGFX_BINDING_TEXTURING 1
+
+/**
+ * @brief Binding number of the "Lighting" uniform.
+ *
+ * @see #mgfx_get_lighting
+ * @see #mgfx_set_lighting_inline
+ */
+#define MGFX_BINDING_LIGHTING 2
+
+/**
+ * @brief Binding number of the "Fog" uniform.
+ *
+ * @see #mgfx_get_fog
+ * @see #mgfx_set_fog_inline
+ */
+#define MGFX_BINDING_FOG 3
+
+/** @brief The maximum number of lights. */
+#define MGFX_LIGHT_COUNT_MAX 8
+
+/// @cond
+
+#define MGFX_LIGHT_POSITION 0
+#define MGFX_LIGHT_POSITIONW 6
+#define MGFX_LIGHT_COLOR 8
+#define MGFX_LIGHT_INTENSITY 14
+#define MGFX_LIGHT_SIZE 16
+
+#define MGFX_MATRIX_SIZE 64
+
+#define MGFX_VTX_TEX_SHIFT 5
+
+/// @endcond
+
+#endif
diff --git a/include/mgfx_macros.h b/include/mgfx_macros.h
new file mode 100644
index 000000000..b256d7e5b
--- /dev/null
+++ b/include/mgfx_macros.h
@@ -0,0 +1,27 @@
+/**
+ * @file mgfx_macros.h
+ * @author Dennis Heinze
+ * @brief Helper macros for mgfx
+ * @ingroup magma
+ */
+
+#ifndef __LIBDRAGON_MGFX_MACROS_H
+#define __LIBDRAGON_MGFX_MACROS_H
+
+#include "mgfx_constants.h"
+
+/** @brief Convert a number fixed point format with f fractional bits. */
+#define MGFX_FIXED_POINT(v, f) ((v)*(1<<(f)))
+
+/** @brief Convert a number to the format used for texture coordinates in mgfx. */
+#define MGFX_TEX_COORD(v) MGFX_FIXED_POINT((v), MGFX_VTX_TEX_SHIFT)
+
+/** @brief Construct a texture coordinate in a format supported by the mgfx shader. */
+#define MGFX_TEX(s, t) { MGFX_TEX_COORD(s), MGFX_TEX_COORD(t) }
+
+/** @brief Construct a vertex normal from normalized integers in a format supported by the mgfx shader. */
+#define MGFX_NRM(x, y, z) ((((x) & 0x1F)<<11) | \
+ (((y) & 0x3F)<<5) | \
+ (((z) & 0x1F)<<0))
+
+#endif
diff --git a/include/mid64.h b/include/mid64.h
new file mode 100644
index 000000000..57760a884
--- /dev/null
+++ b/include/mid64.h
@@ -0,0 +1,148 @@
+/**
+ * @file mid64.h
+ * @author Giovanni Bajo
+ * @brief MID64 sequence player
+ * @ingroup mixer
+ *
+ * MID64 is the compiled form of a Standard MIDI File produced by audioconv64.
+ * The player dispatches MIDI channel events to a #midi_target_t (for example
+ * an SF64 synthesizer) with sample-accurate timing.
+ *
+ * Use #mid64player_play / #mid64player_stop for mixer-driven playback, or
+ * #mid64player_decode_next to walk the stream offline (tests and tooling).
+ */
+#ifndef LIBDRAGON_MID64_H
+#define LIBDRAGON_MID64_H
+
+#include
+#include
+#include "preview.h"
+#include "midi_target.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** @brief Player of a .MID64 file. */
+typedef struct mid64player_s mid64player_t;
+
+/**
+ * @brief Load a MID64 file.
+ * @preview
+ *
+ * Free with #mid64player_close.
+ *
+ * @param fn Filename with filesystem prefix (e.g. `rom:/song.mid64`)
+ * @return Loaded player
+ */
+LIBDRAGON_PREVIEW_API
+mid64player_t *mid64player_load(const char *fn);
+
+/**
+ * @brief Free a player returned by #mid64player_load.
+ * @preview
+ *
+ * Stops playback if active and silences the target. Does not close the
+ * #midi_target_t.
+ *
+ * @param player Player returned by #mid64player_load
+ */
+LIBDRAGON_PREVIEW_API
+void mid64player_close(mid64player_t *player);
+
+/**
+ * @brief Rewind the decoder to the start of the sequence.
+ * @preview
+ *
+ * Restores the default tempo. Call #mid64player_stop first if the player
+ * is currently playing.
+ */
+LIBDRAGON_PREVIEW_API
+void mid64player_rewind(mid64player_t *player);
+
+/**
+ * @brief Start playing the sequence through @p target.
+ * @preview
+ *
+ * Requires #audio_init and #mixer_init. Rewinds the stream. No-op if already
+ * playing.
+ *
+ * @param player Loaded player
+ * @param target Synth or other MIDI backend (must outlive playback)
+ */
+LIBDRAGON_PREVIEW_API
+void mid64player_play(mid64player_t *player, midi_target_t *target);
+
+/**
+ * @brief Stop playback as soon as possible.
+ * @preview
+ *
+ * Silences the target. No-op if not playing.
+ */
+LIBDRAGON_PREVIEW_API
+void mid64player_stop(mid64player_t *player);
+
+/**
+ * @brief Decode the next event and dispatch channel messages to @p target.
+ * @preview
+ *
+ * Tempo meta-events update the player but are not sent to the target.
+ * On end of sequence, returns false without calling the target. The @p now
+ * value passed to the target is the absolute MIDI tick of the event.
+ *
+ * @return true if an event was decoded (including tempo); false on end of sequence
+ */
+LIBDRAGON_PREVIEW_API
+bool mid64player_decode_next(mid64player_t *player, midi_target_t *target);
+
+/**
+ * @brief Loop the whole sequence from the start when it ends.
+ * @preview
+ *
+ * Default is false. Loop points inside the file are not supported.
+ */
+LIBDRAGON_PREVIEW_API
+void mid64player_set_loop(mid64player_t *player, bool loop);
+
+/**
+ * @brief Pulses (ticks) per quarter note.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+uint16_t mid64player_get_ppqn(mid64player_t *player);
+
+/**
+ * @brief Sequence length in ticks.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+uint32_t mid64player_get_duration_ticks(mid64player_t *player);
+
+/**
+ * @brief Sequence length in milliseconds (accounting for the tempo map).
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+uint32_t mid64player_get_duration_ms(mid64player_t *player);
+
+/**
+ * @brief Current tempo in microseconds per quarter note.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+uint32_t mid64player_get_tempo(mid64player_t *player);
+
+/**
+ * @brief Current playback position within the sequence, in milliseconds.
+ * @preview
+ *
+ * When looping, this is the position in the current iteration.
+ */
+LIBDRAGON_PREVIEW_API
+uint32_t mid64player_tell_ms(mid64player_t *player);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/midi_target.h b/include/midi_target.h
new file mode 100644
index 000000000..89979d76e
--- /dev/null
+++ b/include/midi_target.h
@@ -0,0 +1,159 @@
+/**
+ * @file midi_target.h
+ * @author Giovanni Bajo
+ * @brief Abstract MIDI synthesizer target for MID64 playback
+ * @ingroup mixer
+ *
+ * A #midi_target_t is a vtable of channel-voice callbacks used by the MID64
+ * player. The player knows only MIDI events and timing; the target may be an
+ * SF64 synth, a test double, a logger, or another backend.
+ *
+ * During mixer-driven playback, @p now is an absolute sample time. With
+ * #mid64player_decode_next, @p now is the absolute MIDI tick instead.
+ * Optional callbacks may be NULL.
+ */
+#ifndef LIBDRAGON_MIDI_TARGET_H
+#define LIBDRAGON_MIDI_TARGET_H
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct midi_target_s;
+
+/** @brief System-level MIDI reset kind (Universal SysEx, etc.). */
+typedef enum {
+ MIDI_SYSTEM_GM1, ///< General MIDI Level 1 System On
+} midi_system_t;
+
+/** @brief Operations implemented by a MIDI target. */
+typedef struct midi_target_ops_s {
+ /**
+ * @brief Start a note on MIDI channel @p ch.
+ *
+ * @param t Target
+ * @param ch MIDI channel (0–15)
+ * @param key MIDI key (0–127)
+ * @param vel Velocity (1–127; 0 is normalized to note-off by the converter)
+ * @param now Absolute time (see file overview)
+ */
+ void (*note_on)(struct midi_target_s *t, int ch, int key, int vel, int64_t now);
+
+ /**
+ * @brief Release a note on MIDI channel @p ch.
+ *
+ * @param t Target
+ * @param ch MIDI channel (0–15)
+ * @param key MIDI key (0–127)
+ * @param vel Release velocity (0–127)
+ * @param now Absolute time (see file overview)
+ */
+ void (*note_off)(struct midi_target_s *t, int ch, int key, int vel, int64_t now);
+
+ /**
+ * @brief MIDI control change on channel @p ch.
+ *
+ * @param t Target
+ * @param ch MIDI channel (0–15)
+ * @param cc Controller number (0–127)
+ * @param value Controller value (0–127)
+ * @param now Absolute time (see file overview)
+ */
+ void (*control_change)(struct midi_target_s *t, int ch, int cc, int value, int64_t now);
+
+ /**
+ * @brief MIDI program change on channel @p ch.
+ *
+ * @param t Target
+ * @param ch MIDI channel (0–15)
+ * @param program Program number (0–127)
+ * @param now Absolute time (see file overview)
+ */
+ void (*program_change)(struct midi_target_s *t, int ch, int program, int64_t now);
+
+ /**
+ * @brief MIDI pitch bend on channel @p ch.
+ *
+ * @param t Target
+ * @param ch MIDI channel (0–15)
+ * @param value 14-bit bend (0–16383, center 8192)
+ * @param now Absolute time (see file overview)
+ */
+ void (*pitch_bend)(struct midi_target_s *t, int ch, int value, int64_t now);
+
+ /**
+ * @brief Polyphonic key pressure on channel @p ch. May be NULL.
+ *
+ * @param t Target
+ * @param ch MIDI channel (0–15)
+ * @param key MIDI key (0–127)
+ * @param pressure Pressure (0–127)
+ * @param now Absolute time (see file overview)
+ */
+ void (*poly_pressure)(struct midi_target_s *t, int ch, int key, int pressure, int64_t now);
+
+ /**
+ * @brief Channel pressure on channel @p ch. May be NULL.
+ *
+ * @param t Target
+ * @param ch MIDI channel (0–15)
+ * @param pressure Pressure (0–127)
+ * @param now Absolute time (see file overview)
+ */
+ void (*channel_pressure)(struct midi_target_s *t, int ch, int pressure, int64_t now);
+
+ /**
+ * @brief End of sequence: release sounding notes musically. May be NULL.
+ *
+ * Unlike #midi_target_ops_t::reset, this must not hard-cut voices; held
+ * notes and sustain should enter release so tails can finish.
+ *
+ * @param t Target
+ * @param now Absolute time (see file overview)
+ */
+ void (*finish)(struct midi_target_s *t, int64_t now);
+
+ /**
+ * @brief Immediate silence and restore of channel controllers. May be NULL.
+ *
+ * Used for stop, loop restart, and similar hard resets.
+ *
+ * @param t Target
+ * @param now Absolute time (see file overview)
+ */
+ void (*reset)(struct midi_target_s *t, int64_t now);
+
+ /**
+ * @brief Apply a system-level reset (e.g. GM1 System On). May be NULL.
+ *
+ * @param t Target
+ * @param system Which system reset was received
+ * @param now Absolute time (see file overview)
+ */
+ void (*system_reset)(struct midi_target_s *t, midi_system_t system, int64_t now);
+
+ /**
+ * @brief Advance the target to absolute time @p now. May be NULL.
+ *
+ * @param t Target
+ * @param now Absolute time (see file overview)
+ * @return Next absolute time at which the target needs attention,
+ * or `INT64_MAX` if nothing is pending
+ */
+ int64_t (*process)(struct midi_target_s *t, int64_t now);
+} midi_target_ops_t;
+
+/**
+ * @brief Opaque MIDI target (synth backend).
+ */
+typedef struct midi_target_s {
+ const midi_target_ops_t *ops; ///< Backend operations
+} midi_target_t;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/mixer.h b/include/mixer.h
index ceccd9d31..ecf10c5a1 100644
--- a/include/mixer.h
+++ b/include/mixer.h
@@ -8,8 +8,10 @@
#ifndef __LIBDRAGON_MIXER_H
#define __LIBDRAGON_MIXER_H
+
#include
#include
+#include "preview.h"
#ifdef __cplusplus
extern "C" {
@@ -58,6 +60,15 @@ extern "C" {
/** @brief Maximum number of channels supported by the mixer */
#define MIXER_MAX_CHANNELS 32
+/** @brief Priority: always reusable / cheapest to steal. */
+#define MIXER_PRIORITY_MIN 0
+/** @brief Base priority recommended for one-shot sound effects. */
+#define MIXER_PRIORITY_SFX 64
+/** @brief Base priority recommended for music players (e.g. SF64). */
+#define MIXER_PRIORITY_MUSIC 192
+/** @brief Priority: never stolen (default after #mixer_ch_play). */
+#define MIXER_PRIORITY_MAX 255
+
/**
* Number of bytes in sample buffers that must be over-read to make the
* RSP ucode safe.
@@ -107,26 +118,135 @@ void mixer_close(void);
*/
void mixer_set_vol(float vol);
+/**
+ * @brief Scalar ramp curve: value at normalized time @p u ∈ [0, 1].
+ *
+ * Used by #mixer_ch_set_gain_ramp and #mixer_ch_set_freq_ramp. The mixer
+ * samples the curve once per mix round and walks linearly between those
+ * points on the RSP. Built-ins: #mixer_ramp_linear, #mixer_ramp_exp.
+ * Sequencers may pass their own (e.g. SoundFont convex attack).
+ *
+ * @param[in] start Value at u = 0 (already reached when the ramp arms)
+ * @param[in] end Value at u = 1 (the ramp target)
+ * @param[in] u Progress in [0, 1]
+ * @return Interpolated value
+ */
+typedef float (*mixer_ramp_fn_t)(float start, float end, float u);
+
+/**
+ * @brief Linear ramp: `start + (end - start) * u`.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+float mixer_ramp_linear(float start, float end, float u);
+
+/**
+ * @brief Exponential ramp (linear in log): `start * (end/start)^u`.
+ * @preview
+ *
+ * For amplitude this is a constant-dB fade; for frequency, linear in cents.
+ * If @p end is ≤ 0, fades toward silence over the duration (then snaps to 0
+ * when the ramp completes). If @p start is ≤ 0, rises as `end * u`.
+ */
+LIBDRAGON_PREVIEW_API
+float mixer_ramp_exp(float start, float end, float u);
+
/**
* @brief Set channel volume (as left/right).
- *
+ *
* Configure channel volume for the specified channel, specifying two values:
- * one for the left output and one for the right output.
- *
+ * one for the left output and one for the right output. The mixed level is
+ * #mixer_ch_set_gain times these volumes (gain defaults to 1.0).
+ *
* The volume is an attenuation (no amplification is performed).
- * Valid volume range in [0..1], where 0 is silence and 1 is original
- * channel sample volume (no attenuation performed).
- *
+ * Valid volume range in [-1..1], where 0 is silence and 1 is original
+ * channel sample volume (no attenuation performed). Negative volumes
+ * invert the phase of that bus (used by #mixer_ch_set_vol_dolby).
+ *
* Notice that it's perfectly valid to set left/right volumes even if the
* channel itself will play a mono waveforms, as it allows to balance a mono
* sample between the two final output channels.
- *
+ *
* @param[in] ch Channel index
- * @param[in] lvol Left volume (range [0..1])
- * @param[in] rvol Right volume (range [0..1])
+ * @param[in] lvol Left volume (range [-1..1])
+ * @param[in] rvol Right volume (range [-1..1])
*/
void mixer_ch_set_vol(int ch, float lvol, float rvol);
+/**
+ * @brief Linearly ramp channel volume to the specified values.
+ * @preview
+ *
+ * This is the same as #mixer_ch_set_vol, but instead of changing the volume
+ * right away, the mixer walks it linearly from its current value to the
+ * specified one over @p duration output samples. Independent of
+ * #mixer_ch_set_gain_ramp: both apply at once (gain × volume).
+ *
+ * The ramp is run by the mixer itself, across RSP rounds and audio buffers: it
+ * does not need any #mixer_add_event callback while it is in progress, and it
+ * is smooth well below the granularity of a mix round. Use it for stereo fades
+ * and crossfades; for a mono amplitude envelope prefer #mixer_ch_set_gain_ramp.
+ *
+ * A ramp replaces whatever volume ramp was running on the channel, starting
+ * from the volume that one had reached. Calling #mixer_ch_set_vol (or passing
+ * a @p duration of 0 here) cancels the volume ramp and sets the volume
+ * immediately; stopping the channel cancels it as well.
+ *
+ * The RSP walks the volume one output sample at a time, in steps it recomputes
+ * every four of them, so @p duration is effectively rounded up to a multiple
+ * of 4.
+ *
+ * @param[in] ch Channel index
+ * @param[in] lvol Target left volume (range [-1..1])
+ * @param[in] rvol Target right volume (range [-1..1])
+ * @param[in] duration Length of the ramp, in output samples
+ */
+LIBDRAGON_PREVIEW_API
+void mixer_ch_set_vol_ramp(int ch, float lvol, float rvol, int duration);
+
+/**
+ * @brief Set the mono gain of a channel.
+ * @preview
+ *
+ * Gain is a per-channel attenuation in [0..1] (default 1.0), multiplied with
+ * #mixer_ch_set_vol when mixing. The change is immediate: it cancels a running
+ * gain ramp, and does not affect a running volume ramp.
+ *
+ * @param[in] ch Channel index
+ * @param[in] gain Target gain (range [0..1])
+ */
+LIBDRAGON_PREVIEW_API
+void mixer_ch_set_gain(int ch, float gain);
+
+/**
+ * @brief Ramp the mono gain of a channel.
+ * @preview
+ *
+ * Walks gain from its current value to @p gain over @p duration output samples,
+ * independently of #mixer_ch_set_vol_ramp. The mixer runs the ramp by itself
+ * (no #mixer_add_event while it is in progress).
+ *
+ * @p curve maps normalized time to the gain value (#mixer_ramp_linear,
+ * #mixer_ramp_exp, or a custom #mixer_ramp_fn_t).
+ *
+ * A new gain ramp replaces any previous one, starting from the gain already
+ * reached. Duration 0, or #mixer_ch_set_gain, cancels the ramp and sets the
+ * gain immediately; stopping the channel cancels it as well.
+ *
+ * @p started_ago is how many output samples ago this ramp should be treated
+ * as having begun (0 = start now). Use it when catching up an envelope that
+ * was due earlier than the current mixer time.
+ *
+ * @param[in] ch Channel index
+ * @param[in] gain Target gain (range [0..1])
+ * @param[in] duration Length of the ramp, in output samples
+ * @param[in] curve Ramp curve (must not be NULL if duration > 0)
+ * @param[in] started_ago Samples since the ramp's logical start (>= 0)
+ */
+LIBDRAGON_PREVIEW_API
+void mixer_ch_set_gain_ramp(int ch, float gain, int duration,
+ mixer_ramp_fn_t curve, int started_ago);
+
/**
* @brief Set channel volume (as volume and panning).
*
@@ -168,6 +288,42 @@ void mixer_ch_set_vol_pan(int ch, float vol, float pan);
void mixer_ch_set_vol_dolby(int ch, float fl, float fr,
float c, float sl, float sr);
+/**
+ * @brief Enable or disable force-mono on a single channel.
+ * @preview
+ *
+ * When enabled, the channel's output is folded equally onto both output buses
+ * regardless of its panning or stereo routing:
+ * - Mono channel: both buses receive (lvol + rvol) / 2 of the sample.
+ * - Stereo waveform (owner + sub pair): L and R sample streams are each
+ * routed at half amplitude to both buses, yielding
+ * L_out = R_out = 0.5 * (L*lvol + R*rvol).
+ *
+ * Set the flag on the OWNER channel of a stereo pair; the SUB inherits.
+ *
+ * @param[in] ch Channel index
+ * @param[in] enable true to fold, false to restore stereo routing
+ */
+LIBDRAGON_PREVIEW_API
+void mixer_ch_set_force_mono(int ch, bool enable);
+
+/**
+ * @brief Query the force-mono flag for a channel.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+bool mixer_ch_get_force_mono(int ch);
+
+/**
+ * @brief Enable or disable force-mono on every channel at once.
+ * @preview
+ *
+ * Convenience wrapper for a global "Mono" output preference. Applies the
+ * flag to all currently-allocated channels.
+ */
+LIBDRAGON_PREVIEW_API
+void mixer_set_force_mono(bool enable);
+
/**
* @brief Start playing the specified waveform on the specified channel.
*
@@ -190,23 +346,71 @@ void mixer_ch_set_vol_dolby(int ch, float fl, float fr,
*
* @param[in] ch Channel index
* @param[in] wave Waveform to playback
+ *
+ * @see #mixer_ch_stop
*/
void mixer_ch_play(int ch, waveform_t *wave);
/**
* @brief Change the frequency for the specified channel.
- *
+ *
* By default, the frequency is the one required by the waveform associated
- * to the channel, but this function allows to override.
- *
+ * to the channel, but this function allows to override. The change is
+ * immediate and cancels a running #mixer_ch_set_freq_ramp.
+ *
* This function must be called after #mixer_ch_play, as otherwise the
* frequency is reset to the default of the waveform.
- *
+ *
* @param[in] ch Channel index
* @param[in] frequency Playback frequency (in Hz / samples per second)
*/
void mixer_ch_set_freq(int ch, float frequency);
+/**
+ * @brief Ramp the playback frequency of a channel.
+ * @preview
+ *
+ * Walks frequency from its current value to @p frequency over @p duration
+ * output samples. The mixer runs the ramp by itself (no #mixer_add_event
+ * while it is in progress). @p curve selects the shape (#mixer_ramp_linear
+ * in Hz, #mixer_ramp_exp for linear cents, or a custom #mixer_ramp_fn_t).
+ *
+ * A new ramp replaces any previous one, starting from the frequency already
+ * reached. Duration 0, or #mixer_ch_set_freq, cancels the ramp and sets the
+ * frequency immediately; stopping the channel cancels it as well.
+ *
+ * @p started_ago is how many output samples ago this ramp should be treated
+ * as having begun (0 = start now). Use it when catching up an envelope that
+ * was due earlier than the current mixer time.
+ *
+ * @param[in] ch Channel index
+ * @param[in] frequency Target playback frequency (Hz)
+ * @param[in] duration Length of the ramp, in output samples
+ * @param[in] curve Ramp curve (must not be NULL if duration > 0)
+ * @param[in] started_ago Samples since the ramp's logical start (>= 0)
+ */
+LIBDRAGON_PREVIEW_API
+void mixer_ch_set_freq_ramp(int ch, float frequency, int duration,
+ mixer_ramp_fn_t curve, int started_ago);
+
+/**
+ * @brief Enable or disable the sustain loop on a mixer channel.
+ * @preview
+ *
+ * The loop bounds come from the waveform (`#waveform_t::loop_len` /
+ * `#waveform_t::loop_end`). Disabling does not seek: playback continues from
+ * the current position through to `#waveform_t::len` (the release tail, when
+ * present) and then stops. Enabling again restores looping at the waveform's
+ * loop region.
+ *
+ * Must be called while the channel is playing a waveform that defines a loop.
+ *
+ * @param[in] ch Channel index
+ * @param[in] enable True to loop, false to play through to the end
+ */
+LIBDRAGON_PREVIEW_API
+void mixer_ch_set_loop(int ch, bool enable);
+
/**
* @brief Change the current playback position within a waveform.
*
@@ -216,11 +420,16 @@ void mixer_ch_set_freq(int ch, float frequency);
*
* This function must be called after #mixer_ch_play, as otherwise the
* position is reset to the beginning of the waveform.
+ *
+ * Some compressed formats can only restart their decoder on the seek points
+ * their asset was built with: a VADPCM waveform, for instance, accepts the
+ * start of the waveform and its loop point. Seeking elsewhere on one of those
+ * is an error.
*
* @param[in] ch Channel index
* @param[in] pos Playback position (in number of samples)
*/
-void mixer_ch_set_pos(int ch, float pos);
+void mixer_ch_set_pos(int ch, double pos);
/**
* Read the current playback position of the waveform in the channel.
@@ -231,7 +440,7 @@ void mixer_ch_set_pos(int ch, float pos);
* @param[in] ch Channel index
* @return Playback position (in number of samples)
*/
-float mixer_ch_get_pos(int ch);
+double mixer_ch_get_pos(int ch);
/** @brief Stop playing samples on the specified channel. */
void mixer_ch_stop(int ch);
@@ -239,6 +448,72 @@ void mixer_ch_stop(int ch);
/** @brief Return true if the channel is currently playing samples. */
bool mixer_ch_playing(int ch);
+/** @brief Return the waveform being played on this channel, or NULL if none.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+waveform_t* mixer_ch_playing_waveform(int ch);
+
+/**
+ * @brief Set the voice-stealing priority of a channel.
+ * @preview
+ *
+ * Higher values are more expensive to steal. Valid range is
+ * [#MIXER_PRIORITY_MIN]..[#MIXER_PRIORITY_MAX]. #mixer_ch_play resets the
+ * channel to #MIXER_PRIORITY_MAX; #mixer_ch_stop resets it to
+ * #MIXER_PRIORITY_MIN. Players that participate in stealing (e.g. SF64)
+ * lower the priority of their channels before calling #mixer_ch_alloc.
+ *
+ * @param[in] ch Channel index
+ * @param[in] priority Stealing priority
+ */
+LIBDRAGON_PREVIEW_API
+void mixer_ch_set_priority(int ch, int priority);
+
+/**
+ * @brief Plan up to @p count channels inside `[first_ch, first_ch+num_ch)`.
+ * @preview
+ *
+ * Plan-only: nothing is stopped or started. Occupied channels returned in
+ * @p out must be stopped by the caller before reuse. Free channels are
+ * preferred; among occupied ones, lower priority, then quieter instantaneous
+ * volume, then older start time. A channel is stealable only when its
+ * priority is `<= @p priority`.
+ *
+ * If @p stereo is true, each entry in @p out is the primary of a contiguous
+ * pair (`ch`, `ch+1`); the secondary of an existing stereo pair is never
+ * returned as a primary. If @p wave is non-NULL, channels whose configured
+ * limits cannot play it are skipped.
+ *
+ * @param[in] first_ch First channel of the search window
+ * @param[in] num_ch Size of the search window
+ * @param[in] count Maximum number of channels to plan
+ * @param[in] stereo Request contiguous stereo pairs
+ * @param[in] priority Maximum victim priority that may be stolen
+ * @param[in] wave Waveform for limit filtering, or NULL
+ * @param[out] out Buffer of at least @p count ints
+ * @return Number of channels planned (0..@p count)
+ */
+LIBDRAGON_PREVIEW_API
+int mixer_ch_alloc(int first_ch, int num_ch, int count, bool stereo,
+ int priority, waveform_t *wave, int *out);
+
+/**
+ * @brief Play @p wave on an automatically chosen mixer channel.
+ * @preview
+ *
+ * Allocates with #mixer_ch_alloc over the full mixer (stereo deduced from
+ * `wave->channels`), stops any victim, resets volume/pan to `(1,1)` and
+ * clears force-mono, then calls #mixer_ch_play and sets the channel priority
+ * to @p priority.
+ *
+ * @param[in] wave Waveform to play
+ * @param[in] priority Stealing priority for the new voice
+ * @return Channel index, or -1 if none available
+ */
+LIBDRAGON_PREVIEW_API
+int mixer_play(waveform_t *wave, int priority);
+
/**
* @brief Configure the limits of a channel with respect to sample bit size, and
* frequency.
@@ -284,6 +559,58 @@ bool mixer_ch_playing(int ch);
*/
void mixer_ch_set_limits(int ch, int max_bits, float max_frequency, int max_buf_sz);
+/**
+ * @brief Throttle the mixer by specifying the maximum number of samples
+ * @preview
+ * it can generate.
+ *
+ * This is an advanced function that should only be called to achieve perfect
+ * sync between a possibly slowing-down video and audio.
+ *
+ * Normally, once the mixer is initiated and assuming mixer_poll is called
+ * frequently enough, the audio will playback uninterrupted, irrespective of
+ * any slow down in the main loop. This is the expected behavior for background
+ * music for instance, but it does not work for video players or cut-scenes in
+ * which the music must be perfectly synchronized with the video. If the video
+ * happens to slowdown, the music will desynchronize.
+ *
+ * mixer_throttle sets a budget of samples that the mixer is allowed to
+ * generate. Every time the function is called, the specified number of samples
+ * is added to the budget. Every time the mixer playbacks the channel, the
+ * budget is decreased. If the budget reaches zero, the mixer will automatically
+ * pause playback until the budget is increased again, possibly creating
+ * audio cracks.
+ *
+ * To achieve perfect sync, call #mixer_throttle every time a video frame
+ * was generated, and pass the maximum number of samples that the mixer is
+ * allowed to produce. Typically, you will want to pass the audio samplerate
+ * divided by the video framerate, which corresponds to the number of
+ * audio samples per video frame.
+ *
+ * @param[in] num_samples Number of new samples that the mixer is allowed
+ * to produce for this channel. This will be added
+ * to whatever allowance was left.
+ *
+ * @see #mixer_unthrottle
+ */
+LIBDRAGON_PREVIEW_API
+void mixer_throttle(float num_samples);
+
+/**
+ * @brief Unthrottle the mixer
+ * @preview
+ *
+ * Switch back the mixer to the default unthrottled status, after some calls to
+ * #mixer_throttle.
+ *
+ * After calling #mixer_unthrottle, the mixer will no longer be limited and
+ * will produce all the samples requested via #mixer_poll.
+ *
+ * @see #mixer_throttle
+ */
+LIBDRAGON_PREVIEW_API
+void mixer_unthrottle(void);
+
/**
* @brief Run the mixer to produce output samples.
*
@@ -295,17 +622,35 @@ void mixer_ch_set_limits(int ch, int max_bits, float max_frequency, int max_buf_
* A common pattern would be to call #audio_write_begin to obtain an audio
* buffer's pointer, and pass it to mixer_poll.
*
- * mixer_poll performs mixing using RSP. If RSP is busy, mixer_poll will
- * spin-wait until the RSP is free, to perform audio processing.
+ * mixer_poll performs mixing using RSP: it enqueues the mix commands in the
+ * high priority queue and waits for the RSP to execute them before returning.
*
* Since the N64 AI can only be fed with an even number of samples, mixer_poll
* does not accept odd numbers.
*
+ * This function will respect throttling, if configured via #mixer_throttle.
+ * In this case, it may produce less samples than requested, depending on
+ * the current allowance. The rest of the output buffer will be zeroed.
+ *
* @param[in] out Output buffer were samples will be written.
* @param[in] nsamples Number of stereo samples to generate.
*/
void mixer_poll(int16_t *out, int nsamples);
+/**
+ * @brief Request the mixer to try and write audio samples to be played,
+ * @preview
+ * if possible.
+ *
+ * This function is a user helper for asking the mixer and audio subsystems
+ * to play audio during a game frame. You should call this function many times
+ * during one frame (eg. during the render step or after processing each game
+ * object) as many times as necessary. Not polling the audio subsystem often
+ * enough will result in audio stutter.
+ */
+LIBDRAGON_PREVIEW_API
+void mixer_try_play(void);
+
/**
* @brief Callback invoked by mixer_poll at a specified time
*
@@ -355,6 +700,42 @@ void mixer_add_event(int64_t delay, MixerEvent cb, void *ctx);
*/
void mixer_remove_event(MixerEvent cb, void *ctx);
+/**
+ * @brief Callback invoked after every mixer round
+ *
+ * A soft event does not force the mixer to split a round or poll segment.
+ * After each round of @p round_ns output samples has been mixed, every
+ * registered soft callback is invoked with that length so clients can advance
+ * their own clocks (envelopes, sequencers) without affecting mix granularity.
+ *
+ * @param[in] ctx Opaque pointer from #mixer_add_soft_event
+ * @param[in] round_ns Output samples mixed in the round that just ended
+ */
+typedef void (*MixerSoftEvent)(void *ctx, int round_ns);
+
+/**
+ * @brief Register a per-round soft callback
+ * @preview
+ *
+ * The callback is invoked after every mixer round until removed. It must not
+ * call #mixer_poll / #mixer_try_play.
+ *
+ * @param[in] cb Soft callback
+ * @param[in] ctx Opaque pointer passed to @p cb
+ */
+LIBDRAGON_PREVIEW_API
+void mixer_add_soft_event(MixerSoftEvent cb, void *ctx);
+
+/**
+ * @brief Deregister a soft callback
+ * @preview
+ *
+ * @param[in] cb Callback registered via #mixer_add_soft_event
+ * @param[in] ctx Opaque pointer registered with the callback
+ */
+LIBDRAGON_PREVIEW_API
+void mixer_remove_soft_event(MixerSoftEvent cb, void *ctx);
+
/*********************************************************************
*
@@ -411,6 +792,7 @@ void mixer_remove_event(MixerEvent cb, void *ctx);
* @param[in] ctx Opaque pointer that is provided as context to the function,
* and is specified in the waveform.
* @param[in] sbuf Samplebuffer into which read samples should be stored.
+ * You can access the waveform_t instance via sbuf->wave.
* @param[in] wpos Absolute position in the waveform to read from (in samples).
* @param[in] wlen Minimum number of samples to read (in samples).
* @param[in] seeking True if this call requires seeking in the waveform, false
@@ -418,6 +800,34 @@ void mixer_remove_event(MixerEvent cb, void *ctx);
*/
typedef void (*WaveformRead)(void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, bool seeking);
+/**
+ * @brief Notify the start of playback of a waveform.
+ *
+ * This is the callback that will be invoked by the mixer when a waveform
+ * is first "assigned" to a channel. Notice that if the waveform is
+ * then played back multiple times (either by loops or even manual
+ * triggers, even with pauses inbetween), this function is still called
+ * only once.
+ *
+ * @param[in] ctx Opaque pointer that is provided as context to the function,
+ * and is specified in the waveform.
+ * @param[in] sbuf Samplebuffer into which read samples should be stored.
+ */
+typedef void (*WaveformStart)(void *ctx, samplebuffer_t *sbuf);
+
+/** @brief Waveform sample format (stored in #waveform_t::format). */
+typedef enum {
+ /** Interleaved PCM samples (8/16-bit). Default. */
+ WAVEFORM_FORMAT_PCM = 0,
+ /**
+ * Mono or stereo VADPCM: the samplebuffer holds compressed 9-byte frames
+ * (one plane per mixer channel); the mixer ucode decodes them in DMEM
+ * during mixing. Stereo files are block-planar on disk and use two
+ * consecutive mixer channels as independent mono VADPCM streams.
+ */
+ WAVEFORM_FORMAT_VADPCM = 1,
+} waveform_format_t;
+
/**
* @brief A waveform that can be played back through the mixer.
*
@@ -437,7 +847,7 @@ typedef void (*WaveformRead)(void *ctx, samplebuffer_t *sbuf, int wpos, int wlen
* with #mixer_ch_play, they will use automatically two channels (the specified
* one and the following).
*/
-typedef struct waveform_s {
+ typedef struct waveform_s {
/** @brief Name of the waveform (for debugging purposes) */
const char *name;
@@ -445,6 +855,7 @@ typedef struct waveform_s {
* @brief Width of a sample of this waveform, in bits.
*
* Supported values are 8 or 16. Notice that samples must always be signed.
+ * For #WAVEFORM_FORMAT_VADPCM this is the decoded width (always 16).
*/
uint8_t bits;
@@ -456,6 +867,15 @@ typedef struct waveform_s {
*/
uint8_t channels;
+ /**
+ * @brief Sample format (see #waveform_format_t).
+ * @preview
+ *
+ * Defaults to #WAVEFORM_FORMAT_PCM when zero-initialized.
+ */
+ LIBDRAGON_PREVIEW_SYM
+ uint8_t format;
+
/** @brief Desired playback frequency (in samples per second, aka Hz). */
float frequency;
@@ -467,16 +887,48 @@ typedef struct waveform_s {
int len;
/**
- * @brief Length of the loop of the waveform (from the end).
+ * @brief Length of the loop, in samples.
+ *
+ * The loop occupies `[loop_end - loop_len, loop_end)`. With the default
+ * `#loop_end` of zero (meaning `#len`), this is a terminal loop: e.g.
+ * `len==1200` and `loop_len==500` repeats the last 500 samples.
+ *
+ * Loop will be enabled by default during playback if configured here, but
+ * can still be toggled using #mixer_ch_set_loop.
+ */
+ int loop_len;
+
+ /**
+ * @brief Exclusive end of the loop, in samples.
+ * @preview
+ *
+ * The loop occupies `[loop_end - loop_len, loop_end)`. With the default
+ * value of zero, this is a terminal loop: e.g. `len==1200`, `loop_len==500`,
+ * and `loop_end==0`, repeats the last 500 samples.
+ *
+ * Otherwise, set it to create a non-terminal loop. For example, `len==1200`,
+ * `loop_len==500`, and `loop_end==1000`, creates a loop that repeats the
+ * samples from 500 to 1000.
+ *
+ * You can toggle the loop at runtime with #mixer_ch_set_loop, including
+ * during playback. For instance, if you disable a loop while it was
+ * already playing, the playback will continue until the end of the
+ * waveform is reached (thus going through the post-loop tail).
+ */
+ LIBDRAGON_PREVIEW_SYM
+ int loop_end;
+
+ /**
+ * @brief Callback to notify the start of playback of the waveform.
+ * @preview
*
- * This value describes how many samples of the tail of the waveform needs
- * to be played in a loop. For instance, if len==1200 and loop_len=500, the
- * waveform will be played once, and then the last 700 samples will be
- * repeated in loop.
+ * This might be useful to perform one-time initializations of the channel state.
+ * See #WaveformStart for more information.
*/
- int loop_len;
+ LIBDRAGON_PREVIEW_SYM
+ WaveformStart start;
- /**
+ /**
* @brief Read function of the waveform.
*
* This is the callback that will be invoked by the mixer to generate the
@@ -484,8 +936,92 @@ typedef struct waveform_s {
*/
WaveformRead read;
- /** @brief Opaque pointer provided as context to the read function. */
+ /**
+ * @brief Opaque pointer provided as context to the read function.
+ */
void *ctx;
+
+ /**
+ * @brief True if #read returns with its transfer still in flight.
+ * @preview
+ *
+ * Set this when the samples are fetched with an asynchronous PI DMA
+ * (see #samplebuffer_dma_wait). The mixer then fetches ahead of time
+ * (#samplebuffer_prefetch), so that the transfer runs while the CPU does
+ * something else. Leave it false for readers that produce the samples
+ * synchronously (CPU decoding, filesystem reads): fetching those early
+ * would just move the same work around, and waste it on a seek.
+ */
+ LIBDRAGON_PREVIEW_SYM
+ bool async_read;
+
+ /**
+ * @brief True if #read produces samples through an RSP overlay.
+ * @preview
+ *
+ * Set this when the bytes #samplebuffer_append returns are filled by RSP
+ * commands still in the queue (e.g. ULC). The samplebuffer then mirrors
+ * those bytes through the same queue (MIX_COPY) so the copy lands after
+ * the decode. Leave it false for CPU or PI producers.
+ *
+ * SP DMA needs an 8-byte aligned destination, so such a producer must also
+ * leave the write cursor on a boundary it can write to: whatever it keeps
+ * of a decoded block has to be a multiple of #samplebuffer_align_units.
+ * Appending whole blocks does that by itself; #samplebuffer_undo of a
+ * partial block (trimming the samples past the end of the waveform) is
+ * where it has to be rounded, or the next append lands mid-word.
+ */
+ LIBDRAGON_PREVIEW_SYM
+ bool rsp_written;
+
+ /**
+ * @brief Granularity in units of the appends made by #read (0 if unknown).
+ * @preview
+ *
+ * Set this when #read always appends a whole multiple of a fixed chunk
+ * (a codec frame: 1024 samples for ULC, one audioframe for YM64). The
+ * samplebuffer sizes its mirrored tail to MAX(#SAMPLEBUFFER_MARGIN_UNITS,
+ * this), so a single append of that size stays contiguous across the wrap.
+ *
+ * Leave it 0 if the appends have no fixed size (or always fit the default
+ * margin of #SAMPLEBUFFER_MARGIN_UNITS).
+ */
+ LIBDRAGON_PREVIEW_SYM
+ int append_units;
+
+ /**
+ * @brief State size required for this waveform to operate
+ * @preview
+ *
+ * The waveform can request the mixer to allocate a specific amount of
+ * memory for its internal state. This is useful to store a per-channel
+ * state (eg: decompressor state).
+ *
+ * If this variable is not 0, the mixer will allocate the specified amount
+ * of memory for the waveform, and make it available to the read
+ * function via the "state" field of the #samplebuffer_t.
+ */
+ LIBDRAGON_PREVIEW_SYM
+ int state_size;
+
+ /**
+ * @brief Resident sample data in RDRAM, or NULL if streamed.
+ * @preview
+ *
+ * When non-NULL, the mixer addresses these bytes directly (PCM samples
+ * or compressed VADPCM frames) and does not allocate a samplebuffer for
+ * this waveform. #read is still invoked for seeking side-effects when
+ * #state_size is non-zero (e.g. VADPCM predictor state).
+ */
+ LIBDRAGON_PREVIEW_SYM
+ const void *mem;
+
+ ///@cond
+ /// Codec-specific runtime data (format-dependent)
+ void *codec;
+ /// Mixer private state. Do not touch, initialize to zero
+ uint32_t __uuid;
+ ///@endcond
} waveform_t;
/** @brief Maximum number of samples in a waveform */
diff --git a/include/model64.h b/include/model64.h
new file mode 100644
index 000000000..3ed38e40a
--- /dev/null
+++ b/include/model64.h
@@ -0,0 +1,192 @@
+/**
+ * @file model64.h
+ * @brief 3D model loading, animation and drawing
+ * @author Dennis Heinze
+ * @author gamemasterplc
+ * @preview
+ */
+#ifndef __LIBDRAGON_MODEL64_H
+#define __LIBDRAGON_MODEL64_H
+
+
+#include
+#include
+#include "preview.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** @brief Animation slot enumeration for model64 animations */
+typedef enum {
+ MODEL64_ANIM_SLOT_0 = 0,
+ MODEL64_ANIM_SLOT_1 = 1,
+ MODEL64_ANIM_SLOT_2 = 2,
+ MODEL64_ANIM_SLOT_3 = 3
+} model64_anim_slot_t;
+
+/// @cond
+struct model64_s;
+typedef struct model64_s model64_t;
+
+struct mesh_s;
+typedef struct mesh_s mesh_t;
+
+struct primitive_s;
+typedef struct primitive_s primitive_t;
+
+struct model64_node_s;
+typedef struct model64_node_s model64_node_t;
+/// @endcond
+
+LIBDRAGON_PREVIEW_API
+model64_t *model64_load(const char *fn);
+LIBDRAGON_PREVIEW_API
+model64_t *model64_load_buf(void *buf, int sz);
+LIBDRAGON_PREVIEW_API
+void model64_free(model64_t *model);
+LIBDRAGON_PREVIEW_API
+model64_t *model64_clone(model64_t *model);
+
+/**
+ * @brief Return the number of meshes in this model.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+uint32_t model64_get_mesh_count(model64_t *model);
+
+/**
+ * @brief Return the mesh at the specified index.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+mesh_t *model64_get_mesh(model64_t *model, uint32_t mesh_index);
+
+
+/**
+ * @brief Return the number of nodes in this model.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+uint32_t model64_get_node_count(model64_t *model);
+
+/**
+ * @brief Return the node at the specified index.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+model64_node_t *model64_get_node(model64_t *model, uint32_t node_index);
+
+/**
+ * @brief Return the first node with the specified name in the model.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+model64_node_t *model64_search_node(model64_t *model, const char *name);
+
+/**
+ * @brief Sets the position of a node in a model relative to its parent.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+void model64_set_node_pos(model64_t *model, model64_node_t *node, float x, float y, float z);
+
+/**
+ * @brief Sets the rotation of a node in a model relative to its parent in the form of an euler angle (ZYX rotation order) in radians.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+void model64_set_node_rot(model64_t *model, model64_node_t *node, float x, float y, float z);
+
+/**
+ * @brief Sets the rotation of a node in a model relative to its parent in the form of a quaternion.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+void model64_set_node_rot_quat(model64_t *model, model64_node_t *node, float x, float y, float z, float w);
+
+/**
+ * @brief Sets the scale of a node in a model relative to its parent.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+void model64_set_node_scale(model64_t *model, model64_node_t *node, float x, float y, float z);
+
+/**
+ * @brief Gets the transformation matrix between a model's root node and a node in a model.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+void model64_get_node_world_mtx(model64_t *model, model64_node_t *node, float dst[16]);
+
+/**
+ * @brief Return the number of primitives in this mesh.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+uint32_t model64_get_primitive_count(mesh_t *mesh);
+
+/**
+ * @brief Return the primitive at the specified index.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+primitive_t *model64_get_primitive(mesh_t *mesh, uint32_t primitive_index);
+
+/**
+ * @brief Draw an entire model.
+ * @preview
+ *
+ * This will draw all nodes that are contained in the given model while applying the relevant node matrices.
+ */
+LIBDRAGON_PREVIEW_API
+void model64_draw(model64_t *model);
+
+/**
+ * @brief Draw a single mesh.
+ * @preview
+ *
+ * This will draw all of the given mesh's primitives.
+ */
+LIBDRAGON_PREVIEW_API
+void model64_draw_mesh(mesh_t *mesh);
+
+/**
+ * @brief Draw a single node.
+ * @preview
+ *
+ * This will draw a single mesh node.
+ */
+LIBDRAGON_PREVIEW_API
+void model64_draw_node(model64_t *model, model64_node_t *node);
+
+/**
+ * @brief Draw a single primitive.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+void model64_draw_primitive(primitive_t *primitive);
+
+LIBDRAGON_PREVIEW_API
+void model64_anim_play(model64_t *model, const char *anim, model64_anim_slot_t slot, bool paused, float start_time);
+LIBDRAGON_PREVIEW_API
+void model64_anim_stop(model64_t *model, model64_anim_slot_t slot);
+LIBDRAGON_PREVIEW_API
+float model64_anim_get_length(model64_t *model, const char *anim);
+LIBDRAGON_PREVIEW_API
+float model64_anim_get_time(model64_t *model, model64_anim_slot_t slot);
+LIBDRAGON_PREVIEW_API
+float model64_anim_set_time(model64_t *model, model64_anim_slot_t slot, float time);
+LIBDRAGON_PREVIEW_API
+float model64_anim_set_speed(model64_t *model, model64_anim_slot_t slot, float speed);
+LIBDRAGON_PREVIEW_API
+bool model64_anim_set_loop(model64_t *model, model64_anim_slot_t slot, bool loop);
+LIBDRAGON_PREVIEW_API
+bool model64_anim_set_pause(model64_t *model, model64_anim_slot_t slot, bool paused);
+LIBDRAGON_PREVIEW_API
+void model64_update(model64_t *model, float deltatime);
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/mpeg1.h b/include/mpeg1.h
new file mode 100644
index 000000000..4de219c0e
--- /dev/null
+++ b/include/mpeg1.h
@@ -0,0 +1,31 @@
+/**
+ * @file mpeg1.h
+ * @author Giovanni Bajo
+ * @brief RSP-accelerated MPEG video player
+ */
+#ifndef LIBDRAGON_MPEG2_H
+#define LIBDRAGON_MPEG2_H
+
+#include "video.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief MPEG1 video codec
+ *
+ * Register this codec via #video_register_codec to enable MPEG1 video playback
+ * using the video.h API.
+ *
+ * This codec supports MPEG1 video elementary streams with no container. The supported
+ * extension is ".m1v". Do not use this codec with container formats such as MPG
+ * or AVI.
+ */
+extern video_codec_t mpeg1_codec;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/n64sys.h b/include/n64sys.h
index e865c9239..4a4c874d0 100644
--- a/include/n64sys.h
+++ b/include/n64sys.h
@@ -2,20 +2,22 @@
* @file n64sys.h
* @author Jennifer Taylor
* @author Giovanni Bajo
- * @author thekovic
* @brief N64 System Interface
* @ingroup n64sys
*/
#ifndef __LIBDRAGON_N64SYS_H
#define __LIBDRAGON_N64SYS_H
+
#include
#include
#include
#include
#include
+#include "preview.h"
#include "cop0.h"
#include "cop1.h"
+#include "n64types.h"
/**
* @defgroup n64sys N64 System Interface
@@ -56,24 +58,6 @@ extern int __boot_tvtype;
*/
#define KSEG0_START_ADDR ((void*)0x80000000)
-/**
- * @brief A physical address on the MIPS bus.
- *
- * Physical addresses are 32-bit wide, and are used to address the memory
- * space of the MIPS R4300 CPU. The MIPS R4300 CPU has a 32-bit address bus,
- * and can address up to 4 GiB of memory.
- *
- * Physical addresses are just numbers, they cannot be used as pointers (dereferenced).
- * To access them, you must first convert them virtual addresses using the
- * #VirtualCachedAddr or #VirtualUncachedAddr macros.
- *
- * In general, libdragon will try to use #phys_addr_t whenever a physical
- * address is expected or returned, and C pointers for virtual addresses.
- * Unfortunately, not all codebase can be changed to follow this convention
- * for backward compatibility reasons.
- */
-typedef uint32_t phys_addr_t;
-
/**
* @brief Return the physical memory address for a given virtual address (pointer)
*
@@ -120,6 +104,22 @@ typedef uint32_t phys_addr_t;
*/
#define VirtualUncachedAddr(_addr) ((void *)(((unsigned long)(_addr))|0xA0000000))
+/**
+ * @brief Create a virtual addresses in a 64-bit uncached segment to access a physical address
+ *
+ * This function is similar to #VirtualUncachedAddr, but it returns a 64-bit
+ * virtual address (#vaddr64_t) instead of a 32-bit address (pointer). This is
+ * useful to access specific portions of the physical address space that are
+ * not accessible using 32-bit addresses, like the upper part of the PI space.
+ *
+ * Use sys_vaddr_readN and sys_vaddr_writeN to read from and write to 64-bit virtual
+ * addresses.
+ *
+ * @param[in] _addr Physical address to convert to a virtual address
+ *
+ * @return A 64-bit virtual address to access the physical address
+ */
+ #define VirtualUncachedAddr64(_addr) ((vaddr64_t)(((_addr))|0x9000000000000000ull))
/**
* @brief Return the uncached memory address for a given virtual address
@@ -289,15 +289,61 @@ inline bool sys_bbplayer(void) {
}
/**
- * @brief Read the number of ticks since system startup
+ * @brief Read the number of ticks since system startup (wall time)
*
+ * This function reads the number of overall ticks since system startup. This
+ * is normally called "wall time", as it includes all the time spent by the CPU,
+ * including all the wait/spin loops and interrupts.
+ *
* The frequency of this counter is #TICKS_PER_SECOND. The counter will
* never overflow, being a 64-bit number.
- *
+ *
* @return The number of ticks since system startup
+ * @see #get_user_ticks
+ * @see #get_system_ticks
*/
uint64_t get_ticks(void);
+/**
+ * @brief Read the number of ticks since system startup (user time)
+ * @preview
+ *
+ * This function is similar to #get_ticks, but it returns the number of ticks
+ * spent in "user time", that is excluding the "system time". This is useful
+ * to measure the actual CPU time spent doing an operation, subtracting all the
+ * various wait/spin loops and interrupts.
+ *
+ * For instance, calling #get_user_ticks() once at each main loop iteration,
+ * and then subtracting the result from the previous iteration, will give the
+ * actual CPU time spent preparing the frame, excluding all the waiting for
+ * vblank or for RSP to finish its work and interrupts.
+ *
+ * @return The number of ticks since system startup (user time)
+ * @see #get_ticks
+ * @see #get_system_ticks
+ */
+LIBDRAGON_PREVIEW_API
+uint64_t get_user_ticks(void);
+
+/**
+ * @brief Read the number of system ticks since system startup (system time)
+ * @preview
+ *
+ * This function returns the number of ticks spent in "system time", that is
+ * the time spent in wait/spin loops and interrupts. This is useful to measure
+ * how much time the CPU is not doing actual work and is just waiting for hardware
+ * components to finish their tasks.
+ *
+ * @return The number of system ticks since system startup
+ * @see #get_ticks
+ * @see #get_user_ticks
+ */
+LIBDRAGON_PREVIEW_API
+inline uint64_t get_system_ticks(void) {
+ extern uint64_t __acct_system_ticks;
+ return __acct_system_ticks;
+}
+
/**
* @brief Read the number of microseconds since system startup
*
@@ -309,12 +355,12 @@ uint64_t get_ticks(void);
uint64_t get_ticks_us(void);
/**
- * @brief Read the number of millisecounds since system startup
+ * @brief Read the number of milliseconds since system startup
*
* This is similar to #get_ticks, but converts the result in integer
* milliseconds for convenience.
*
- * @return The number of millisecounds since system startup
+ * @return The number of milliseconds since system startup
*/
uint64_t get_ticks_ms(void);
@@ -444,6 +490,18 @@ void inst_cache_hit_writeback(volatile const void *, unsigned long);
void inst_cache_hit_invalidate(volatile void *, unsigned long);
/**
+ * @brief Force an instruction cache fill over a memory region
+ * @preview
+ *
+ * @param[in] addr
+ * Pointer to memory in question
+ * @param[in] length
+ * Length in bytes of the data pointed at by addr
+ */
+ LIBDRAGON_PREVIEW_API
+ void inst_cache_hit_fill(volatile void *, unsigned long);
+
+ /**
* @brief Force an instruction cache index invalidate over a memory region
*
* @param[in] addr
@@ -504,12 +562,24 @@ void assert_memory_expanded(void);
* @brief Heap statistics
*/
typedef struct {
- int total; ///< Total heap size in bytes
- int used; ///< Used heap size in bytes
+ int total; ///< Total heap size in bytes
+ int used; ///< Used heap size in bytes
+ LIBDRAGON_PREVIEW_SYM
+ int free; ///< Free heap size in bytes @preview
+ LIBDRAGON_PREVIEW_SYM
+ int fragmented; ///< Free bytes in malloc chunks that are not in the top chunk @preview
+ LIBDRAGON_PREVIEW_SYM
+ float fragmentation;///< Fragmentation factor, in range [0, 1] @preview
} heap_stats_t;
/**
* @brief Return information about memory usage of the heap
+ *
+ * The fragmentation factor is computed from newlib's malloc arena as
+ * `fragmented / malloc_free`, where `fragmented` is the amount of free memory
+ * that is not part of the top chunk (`mallinfo().fordblks - mallinfo().keepcost`).
+ * This estimates how much malloc-managed free memory is trapped in internal
+ * holes rather than available as one expandable tail.
*/
void sys_get_heap_stats(heap_stats_t *stats);
@@ -560,6 +630,24 @@ void *malloc_uncached_aligned(int align, size_t size);
*/
void free_uncached(void *buf);
+/**
+ * @brief Reallocate an uncached memory buffer
+ * @preview
+ *
+ * This function changes the size of the memory buffer pointed to by
+ * `old_buf` to the size specified by `new_size`. The contents will be
+ * unchanged up to the minimum of the old and new sizes.
+ *
+ * @param [in] old_buf Pointer to the previously allocated buffer
+ * @param [in] new_size New size of the buffer
+ * @return A pointer to the reallocated buffer (in the uncached segment) or
+ * NULL if the reallocation failed (in which case the old buffer is
+ * unchanged)
+ */
+LIBDRAGON_PREVIEW_API
+void *realloc_uncached(void *old_buf, size_t new_size);
+
+
/** @brief Type of TV video output */
typedef enum {
TV_PAL = 0, ///< Video output is PAL
@@ -596,21 +684,145 @@ typedef enum {
*/
reset_type_t sys_reset_type(void);
+/**
+ * @brief Get the PI address of the main ELF in ROM
+ * @preview
+ *
+ * This function returns the PI address of the main ELF in ROM,
+ * that is, the address where the running application has been loaded from.
+ *
+ * This is only useful in some very niche cases, eg. for manually loading
+ * sections of the ELF at runtime, or inspecting custom ROM layouts.
+ *
+ * Use #dma_read or #io_read to access the ROM contents at this address space.
+ *
+ * @return Address of the the ELF in PI space (ROM)
+ */
+LIBDRAGON_PREVIEW_API
+pi_addr_t sys_elf_address(void);
+
+/**
+ * @brief Libdragon version information
+ *
+ * This structure contains information about the current version of Libdragon,
+ * that was embedded in the ROM at build time.
+ */
+typedef struct {
+ char branch[32+1]; ///< Branch name (normally "stable" or "preview")
+ char hash[20+1]; ///< Commit hash (SHA1)
+ char commit_date[16+1]; ///< Commit date (YYYY-MM-DD)
+ bool dirty; ///< True if Libdragon repository was dirty at build time
+} sys_version_t;
+
+/**
+ * @brief Get the version of Libdragon
+ * @preview
+ *
+ * This function will fill the version structure with the information about
+ * the current version of Libdragon, that was embedded in the ROM at build time.
+
+ * @param version Pointer to the version structure to fill
+ * @return true if the version information was successfully retrieved, false otherwise
+ */
+LIBDRAGON_PREVIEW_API
+bool sys_get_version(sys_version_t *version);
+
+/**
+ * @brief Perform a hardware-accelerated memory set
+ * @preview
+ *
+ * This function uses a special function in the RCP (MI repeat mode)
+ * to perform a fast memset operation. The actual speed is about 6x
+ * a standard 64-bit memeset, and 12x a 32-bit memset.
+ *
+ * You can use both cached and uncached memory addresses. For cached
+ * addresses, full cache coherency is guaranteed (so it will behave
+ * like a CPU memset would do).
+ *
+ * All the sys_hw_memsetN functions run at the same speed, so this
+ * function is just as fast as #sys_hw_memset64. You don't need to
+ * use the 64-bit version unless you have a 64-bit pattern to repeat.
+ *
+ * @note This special mode is not supported on the iQue player, so this
+ * function falls back to a standard memset when run on iQue.
+ *
+ * @param ptr Pointer to the memory area to set
+ * @param value Value to repeat across the memory area
+ * @param len Length of the memory area in bytes
+ * @return The same pointer passed to it
+ *
+ * @see #sys_hw_memset16
+ * @see #sys_hw_memset32
+ * @see #sys_hw_memset64
+ */
+LIBDRAGON_PREVIEW_API
+void* sys_hw_memset(void *ptr, uint8_t value, size_t len);
+
+/**
+ * @brief Perform a hardware-accelerated memory set of a 16-bit pattern
+ * @preview
+ *
+ * This function is similar to #sys_hw_memset, but repeats a 16-bit
+ * value instead of an 8-bit value. For instance, doing a memset
+ * of value 0xAABB for 7 bytes will result in the following
+ * memory contents: AA BB AA BB AA BB AA
+ *
+ * @param ptr Pointer to the memory area to set
+ * @param value 16-bit value to repeat across the memory area
+ * @param len Length of the memory area in bytes
+ * @return The same pointer passed to it
+ */
+LIBDRAGON_PREVIEW_API
+void* sys_hw_memset16(void *ptr, uint16_t value, size_t len);
+
+/**
+ * @brief Perform a hardware-accelerated memory set of a 32-bit pattern
+ * @preview
+ *
+ * This function is similar to #sys_hw_memset, but repeats a 32-bit
+ * value instead of an 8-bit value. For instance, doing a memset
+ * of value 0xAABBCCDD for 7 bytes will result in the following
+ * memory contents: AA BB CC DD AA BB CC
+ *
+ * @param ptr Pointer to the memory area to set
+ * @param value 32-bit value to repeat across the memory area
+ * @param len Length of the memory area in bytes
+ * @return The same pointer passed to it
+ */
+LIBDRAGON_PREVIEW_API
+void* sys_hw_memset32(void *ptr, uint32_t value, size_t len);
+
+/**
+ * @brief Perform a hardware-accelerated memory set of a 64-bit pattern
+ * @preview
+ *
+ * This function is similar to #sys_hw_memset, but repeats a 64-bit
+ * value instead of an 8-bit value. For instance, doing a memset
+ * of value 0xAABBCCDD11223344 for 11 bytes will result in the following
+ * memory contents: AA BB CC DD 11 22 33 44 AA BB CC
+ *
+ * @param ptr Pointer to the memory area to set
+ * @param value 64-bit value to repeat across the memory area
+ * @param len Length of the memory area in bytes
+ * @return The same pointer passed to it
+ */
+LIBDRAGON_PREVIEW_API
+void* sys_hw_memset64(void *ptr, uint64_t value, size_t len);
+
/** @cond */
+
+/* Error out if srand(time(NULL)) is used. We cannot */
+#define srand(seed) ({ \
+ if (strstr(#seed, "time") && strstr(#seed, "NULL")) \
+ assertf(0, "srand(time(NULL)) will not work on N64 where RTC is not guaranteed. Use srand(getentropy32()) instead"); \
+ srand(seed); \
+})
+
/* Deprecated version of get_ticks */
__attribute__((deprecated("use get_ticks instead")))
-static inline volatile unsigned long read_count(void) {
+static inline unsigned long read_count(void) {
return get_ticks();
}
-
-/* Deprecated functions to tell libdragon which CIC is installed.
- This was only used to cope with differences in boot flags with
- official IPL3s, but it's not required anymore with open source
- IPL3. */
-__attribute__((deprecated("querying CIC type is not supported")))
-static inline int sys_get_boot_cic() { return 6102; }
-__attribute__((deprecated("cannot set CIC type at runtime, but this is not required anymore")))
-static inline void sys_set_boot_cic(int bc) {}
/** @endcond */
#ifdef __cplusplus
diff --git a/include/n64types.h b/include/n64types.h
index f4f7b8e4f..00e867cd9 100644
--- a/include/n64types.h
+++ b/include/n64types.h
@@ -42,6 +42,59 @@ typedef uint32_t u_uint32_t __attribute__((aligned(1)));
*/
typedef uint16_t u_uint16_t __attribute__((aligned(1)));
+/**
+ * @brief A physical address on the MIPS bus.
+ *
+ * Physical addresses are 32-bit wide, and are used to address the memory
+ * space of the MIPS R4300 CPU. The MIPS R4300 CPU has a 32-bit address bus,
+ * and can address up to 4 GiB of memory.
+ *
+ * Physical addresses are just numbers, they cannot be used as pointers (dereferenced).
+ * To access them, you must first convert them virtual addresses using the
+ * #VirtualCachedAddr or #VirtualUncachedAddr macros.
+ *
+ * In general, libdragon will try to use #phys_addr_t whenever a physical
+ * address is expected or returned, and C pointers for virtual addresses.
+ * Unfortunately, not all codebase can be changed to follow this convention
+ * for backward compatibility reasons.
+ */
+typedef uint32_t phys_addr_t;
+
+/**
+ * @brief A PI address (on the peripheral bus)
+ *
+ * The peripheral bus (PI) is a 32-bit address space used to address
+ * devices on the cartridge slot or the bottom slot (N64DD).
+ *
+ * Peripherals connected to the PI bus can answer to any address in this address
+ * space. Cartridge ROMs are usually mapped at 0x10000000.
+ *
+ * Accessing PI addresses is only possible via DMA or CPU I/O operations, as performed
+ * by #dma_read, #dma_write, #io_read, and #io_write.
+ *
+ * I/O operations (with CPU) are only possible with 32-bit access width, and only
+ * for a subset of PI addresses that are memory mapped to the CPU's address space.
+ * Check #io_accessible for a list.
+ */
+typedef uint32_t pi_addr_t;
+
+/**
+ * @brief A 64-bit virtual address (aka "pointer")
+ *
+ * VR4300 CPU supports a full 64-bit virtual address space, but this is
+ * useful only in some very niche use cases. To avoid wasting memory and
+ * performance, Libdragon uses a ILP32 ABI, meaning that C pointers are 32-bit
+ * wide (which works because MIPS64 address space has been designed to be
+ * backwards compatible with MIPS32).
+ *
+ * In the rare cases where a full 64-bit virtual address is needed, we cannot
+ * use a C pointer, so this type is provided instead. To access the memory
+ * at this address, you must use the sys_vaddr_read* and sys_vaddr_write*
+ * functions.
+ */
+typedef uint64_t vaddr64_t;
+
+
#ifdef __cplusplus
}
#endif
diff --git a/include/nand.h b/include/nand.h
new file mode 100644
index 000000000..6c0445ab5
--- /dev/null
+++ b/include/nand.h
@@ -0,0 +1,284 @@
+/**
+ * @file nand.h
+ * @author Giovanni Bajo
+ * @brief iQue NAND (flash) support
+ * @ingroup ique
+ *
+ * This module contains low-level functions to perform read/write operations
+ * on the iQue flash memory (NAND). The flash memory is used to store the
+ * operating system and the games. Contents in the flash are arranged using
+ * a custom filesystem called BBFS, which is implemented in the bbfs.h module.
+ */
+
+/**
+ * @defgroup ique iQue-specific hardware interfaces
+ * @ingroup libdragon
+ * @brief Hardware interfaces and libraries specific for the iQue Player
+ *
+ * This module contains both lower-level hardware interfaces and higher-level
+ * libraries that are specific to the iQue Player (also called "BB player").
+ * It is a console that was released in China that is compatible with the
+ * original N64 hardware while being faster (higher clock, faster RAM).
+ *
+ * Libdragon supports iQue transparently: all ROMs built with libdragon will
+ * also run on the iQue player without modifications (and be even faster!).
+ *
+ * Given that the iQue player also has specific additional hardware components,
+ * libdragon also features some libraries to access them and use them in your
+ * programs (though, at that point, your program will not be compatible with
+ * the original N64).
+ *
+ * To perform a runtime check on what console the ROM is running, just use
+ * the #sys_bbplayer function, that will return true when running on iQue.
+ *
+ */
+#ifndef LIBDRAGON_BB_NAND_H
+#define LIBDRAGON_BB_NAND_H
+
+#include
+#include
+#include "preview.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief An absolute address in the NAND.
+ *
+ * The address can be interpreted as a simple offset byte address from the
+ * start of the NAND, but given the hardware layout of the NAND, it can be
+ * thought as composed of three components:
+ *
+ * - Block number (12 bits)
+ * - Page number (5 bits)
+ * - Offset within the page (9 bits)
+ *
+ * You can use #NAND_ADDR_MAKE to build an address from the three
+ * components, and #NAND_ADDR_BLOCK, #NAND_ADDR_PAGE, #NAND_ADDR_OFFSET
+ * to extract the components from an address.
+ */
+typedef uint32_t nand_addr_t;
+
+#define NAND_BLOCK_SIZE 0x4000 ///< Size of a NAND block in bytes
+#define NAND_PAGE_SIZE 0x200 ///< Size of a NAND page in bytes
+
+/** @brief Build a NAND address given the block, page and offset. */
+#define NAND_ADDR_MAKE(block, page, offset) (((block) << 14) | ((page) << 9) | (offset))
+/** @brief Extract the page offset from an address */
+#define NAND_ADDR_OFFSET(addr) (((addr) >> 0) & 0x1FF)
+/** @brief Extract the page index from an address */
+#define NAND_ADDR_PAGE(addr) (((addr) >> 9) & 0x01F)
+/** @brief Extract the block index from an address */
+#define NAND_ADDR_BLOCK(addr) (((addr) >> 14) & 0xFFF)
+
+/** @brief Flags used for #nand_mmap */
+typedef enum {
+ NAND_MMAP_ENCRYPTED = (1<<0), ///< Data in the filesystem will be decrypted by the mmap
+} nand_mmap_flags_t;
+
+/**
+ * @brief Initialize the library to access the NAND flash on the iQue Player
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+void nand_init(void);
+
+/**
+ * @brief Return the size of the installed NAND.
+ * @preview
+ *
+ * @return Size of the NAND in bytes (either 64 MiB or 128 MiB)
+ */
+LIBDRAGON_PREVIEW_API
+int nand_get_size(void);
+
+/**
+ * @brief Read one or multiple full pages from the NAND
+ * @preview
+ *
+ * This is the lower level function to read data from the NAND. It reads
+ * only full pages and optionally perform ECC correction while reading them.
+ *
+ * Each page of the flash contains 1024 bytes of data (#NAND_PAGE_SIZE) and
+ * 16 bytes of so-called "spare" data. The ECC codes are stored in 6 bytes of
+ * spare data, while some of the other bytes seem used by the official iQue OS
+ * for marking bad blocks or other not fully understood purposes.
+ *
+ * You can read the spare data by providing a pointer to a buffer where to
+ * store it (16 bytes per each requested page). If you don't need the spare data,
+ * you can pass NULL. Notice that ECC correction can be performed even if you
+ * pass NULL to spare.
+ *
+ * You can pass NULL to @p buffer if you don't need the data (for instance, to
+ * only read the spares). Passing NULL to both @p buffer and @p spare can be
+ * useful to only perform an ECC check on pages without actually reading the
+ * data.
+ *
+ * @param addr Address to read from (use NAND_ADDR_MAKE to build)
+ * @param npages Number of pages to read
+ * @param buffer If not NULL, Buffer to read data into (1024 bytes per page, aka #NAND_PAGE_SIZE)
+ * @param spare If not NULL, read also the spare area into the specified buffer (16 bytes per page)
+ * @param ecc Whether to use ECC to correct/verify errors
+ * @return >=0 If OK (number of pages read)
+ * @return -1 If at least one page had an unrecoverable ECC error
+ */
+LIBDRAGON_PREVIEW_API
+int nand_read_pages(nand_addr_t addr, int npages, void *buffer, void *spare, bool ecc);
+
+/**
+ * @brief Write pages to the NAND.
+ * @preview
+ *
+ * Writing to NAND is well defined only on erased blocks. If you write to a
+ * non-erased block, the data will likely be corrupted.
+ *
+ * While technically it is possible to write even data smaller than a page,
+ * that doesn't allow the flash controller to recalculate the ECC, so
+ * it is not recommended because ECC is required to detect failures.
+ *
+ * @param addr Address to write to (use NAND_ADDR_MAKE to build)
+ * @param npages Number of pages to write
+ * @param buffer Buffer to write data from
+ * @param ecc Whether to compute and write ECC for the pages
+ *
+ * @return >=0 If OK (number of pages written)
+ * @return -1 If error during writing
+ */
+LIBDRAGON_PREVIEW_API
+int nand_write_pages(nand_addr_t addr, int npages, const void *buffer, bool ecc);
+
+/**
+ * @brief Erase a block on the NAND.
+ * @preview
+ *
+ * You must erase a block before being able to write to it. Notice that erasing
+ * only works on the whole block, so all the pages inside are erased.
+ *
+ * @param addr Address of the block to erase (must be a block address)
+ *
+ * @return 0 if OK, -1 if error
+ */
+LIBDRAGON_PREVIEW_API
+int nand_erase_block(nand_addr_t addr);
+
+/**
+ * @brief Read sequential data from the NAND.
+ * @preview
+ *
+ * This function reads a sequence of bytes from the NAND, across different
+ * pages and/or blocks. It will only fetch the requested bytes.
+ *
+ * Notice that it is not possible to check ECC when using this function,
+ * because ECC only works when reading full pages. Not using ECC while reading
+ * might cause data corruption if the NAND has errors, so carefully consider
+ * whether to use this function.
+ *
+ * @param addr Address to read from (use NAND_ADDR_MAKE to build)
+ * @param buffer Buffer to read data into
+ * @param len Number of bytes to read
+ *
+ * @return 0 if OK, -1 if error
+ */
+LIBDRAGON_PREVIEW_API
+int nand_read_data(nand_addr_t addr, void *buffer, int len);
+
+/**
+ * @brief Initialize configuration of the NAND memory mapping to PI address space.
+ * @preview
+ *
+ * On iQue, a special hardware component called the ATB (Address Translation
+ * Buffer?) allows to memory map blocks of flash to the PI bus. This allows
+ * for instance for a ROM to be mapped at 0x10000000, which is required for
+ * booting them.
+ *
+ * The configuration must be done from scratch every time it is changed; it
+ * is thus necessary to call #nand_mmap_begin to initialize the configuration,
+ * then call #nand_mmap for each block to map, and finally call #nand_mmap_end
+ * to finish the configuration.
+ *
+ * @see #nand_mmap
+ * @see #nand_mmap_end
+ */
+LIBDRAGON_PREVIEW_API
+void nand_mmap_begin(void);
+
+/**
+ * @brief Memory-map flash blocks to PI address space via ATB.
+ * @preview
+ *
+ * On iQue, a special hardware component called the ATB (Address Translation
+ * Buffer?) allows to memory map blocks of flash to the PI bus. This allows
+ * for instance for a ROM to be mapped at 0x10000000, which is required for
+ * booting them.
+ *
+ * This function configures a specific mapping from a sequence of blocks to
+ * a PI address area. The flash blocks can be non consecutive, though there
+ * are some limits to the number of ATB entries that can be configured,
+ * so it is better to use consecutive blocks if possible.
+ *
+ * If multiple calls to #nand_mmap are done, they must be done in increasing
+ * @a pi_address order.
+ *
+ * The BBFS filesystem is configured to use consecutive blocks as much as possible
+ * for large files (> 512 KiB). To map a file from a NAND formatted with the
+ * BBFS filesystem, you can use #bbfs_get_file_blocks to get the list of blocks
+ * to map.
+ *
+ * @param pi_address PI address to map the blocks to
+ * @param blocks Array of block numbers to map, terminated by -1
+ * @param flags Flags to control the mapping (#NAND_MMAP_ENCRYPTED)
+ * @return 0 if OK, or -1 in case of error. A possible error
+ * is that there are not enough ATB entries available. In
+ * this case, it is necessary to defragment the file on
+ * the NAND (as a single ATB entry can map multiple
+ * consecutive blocks).
+ *
+ * @see #nand_mmap_begin
+ * @see #nand_mmap_end
+ */
+LIBDRAGON_PREVIEW_API
+int nand_mmap(uint32_t pi_address, int16_t *blocks, nand_mmap_flags_t flags);
+
+/**
+ * @brief Finish configuration of the NAND memory mapping to PI address space.
+ * @preview
+ *
+ * On iQue, a special hardware component called the ATB (Address Translation
+ * Buffer?) allows to memory map blocks of flash to the PI bus. This allows
+ * for instance for a ROM to be mapped at 0x10000000, which is required for
+ * booting them.
+ *
+ * This function must be called to finish the configuration. Notice that it
+ * must be called even if #nand_mmap failed.
+ */
+LIBDRAGON_PREVIEW_API
+void nand_mmap_end(void);
+
+/**
+ * @brief Compute the ECC code for a page of data.
+ * @preview
+ *
+ * iQue NAND contains a 6-byte ECC code for each 1024-byte page. This is actually
+ * the combination of two 3-byte ECC for each 512-byte half of the page. The
+ * code is stored in the spare date of each page (bytes 0x8-0xA contain the
+ * ECC of the second half, and bytes 0xD-0xF contain the ECC of the first half).
+ *
+ * This function implements the same algorithm and is provided for completeness.
+ * It is normally not required to compute the ECC code manually, as the flash
+ * controller will do that automatically when writing to the NAND (via
+ * #nand_write_pages) and used to correct errors when reading from the NAND
+ * (via #nand_read_pages).
+ *
+ * @param buf Buffer containing the 1024-byte page
+ * @param ecc Buffer to store the 6-byte ECC code
+ */
+LIBDRAGON_PREVIEW_API
+void nand_compute_page_ecc(const uint8_t *buf, uint8_t *ecc);
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/newlib_overrides/sys/utime.h b/include/newlib_overrides/sys/utime.h
new file mode 100644
index 000000000..3c7a9687b
--- /dev/null
+++ b/include/newlib_overrides/sys/utime.h
@@ -0,0 +1,27 @@
+/**
+ * @file utime.h
+ * @author Giovanni Bajo
+ */
+#ifndef _SYS_UTIME_H
+#define _SYS_UTIME_H
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** @brief Structure for file access and modification times */
+struct utimbuf
+{
+ time_t actime;
+ time_t modtime;
+};
+
+int utime(const char *path, const struct utimbuf *times);
+
+#ifdef __cplusplus
+};
+#endif
+
+#endif /* _SYS_UTIME_H */
diff --git a/include/newlib_overrides/threads.h b/include/newlib_overrides/threads.h
new file mode 100644
index 000000000..657f461aa
--- /dev/null
+++ b/include/newlib_overrides/threads.h
@@ -0,0 +1,143 @@
+/**
+ * @file threads.h
+ * @author Giovanni Bajo
+ * @brief Newlib override for threads.h, for C11 thread implementation.
+ */
+#ifndef LIBDRAGON_KERNEL_CTHREADS_H
+#define LIBDRAGON_KERNEL_CTHREADS_H
+
+#include
+#include "n64sys.h"
+#include "kernel.h"
+
+/** @brief Default stack size for threads */
+#ifndef CTHREADS_DEFAULT_STACK_SIZE
+#define CTHREADS_DEFAULT_STACK_SIZE (4*1024)
+#endif
+
+/** @brief Thread handle type */
+typedef kthread_t* thrd_t;
+
+enum {
+ thrd_success = 0,
+ thrd_nomem = -1,
+ thrd_timedout = -2,
+ thrd_busy = -3,
+ thrd_error = -999,
+};
+
+/** @brief Thread start function type */
+typedef int (*thrd_start_t)(void*);
+
+/** @brief Create a thread with extended parameters */
+int thrd_create_ex(thrd_t *thr, const char *name, int stack_size, int pri, thrd_start_t func, void *arg);
+
+/** @brief Create a thread with default parameters */
+inline int thrd_create(thrd_t *thr, thrd_start_t func, void *arg)
+{
+ return thrd_create_ex(thr, "", CTHREADS_DEFAULT_STACK_SIZE, 0, func, arg);
+}
+
+/** @brief Compare two thread handles for equality */
+inline int thrd_equal(thrd_t lhs, thrd_t rhs)
+{
+ return lhs == rhs;
+}
+
+/** @brief Get the current thread handle */
+inline thrd_t thrd_current(void) {
+ return kthread_current();
+}
+
+/** @brief Yield the current thread */
+inline void thrd_yield(void) {
+ kthread_yield();
+}
+
+/** @brief Exit the current thread */
+__attribute__((noreturn))
+inline void thrd_exit(int res)
+{
+ kthread_exit(res);
+}
+
+/** @brief Detach a thread */
+inline int thrd_detach(thrd_t thr)
+{
+ kthread_detach(thr);
+ return thrd_success;
+}
+
+/** @brief Join a thread */
+int thrd_join(thrd_t thr, int *res);
+/** @brief Sleep for a specified duration */
+int thrd_sleep(const struct timespec* duration, struct timespec* remaining);
+
+/** @brief Mutex type */
+typedef kmutex_t mtx_t;
+
+enum {
+ mtx_plain = 1<<0,
+ mtx_recursive = 1<<1,
+ mtx_timed = 1<<2,
+};
+
+/** @brief Initialize a mutex */
+int mtx_init(mtx_t *mutex, int type);
+
+/** @brief Lock a mutex */
+inline int mtx_lock(mtx_t* mutex) {
+ kmutex_lock(mutex);
+ return thrd_success;
+}
+/** @brief Try to lock a mutex */
+inline int mtx_trylock(mtx_t *mutex) {
+ return kmutex_try_lock(mutex, 0) ? thrd_success : thrd_busy;
+}
+
+/** @brief Lock a mutex with timeout */
+int mtx_timedlock(mtx_t *restrict mutex, const struct timespec *restrict time_point);
+
+/** @brief Unlock a mutex */
+inline int mtx_unlock(mtx_t *mutex) {
+ kmutex_unlock(mutex);
+ return thrd_success;
+}
+/** @brief Destroy a mutex */
+inline void mtx_destroy(mtx_t *mutex) {
+ kmutex_destroy(mutex);
+}
+
+/** @brief Condition variable type */
+typedef kcond_t cnd_t;
+
+/** @brief Initialize a condition variable */
+inline int cnd_init(cnd_t *cond) {
+ kcond_init(cond);
+ return thrd_success;
+}
+/** @brief Signal a condition variable */
+inline int cnd_signal(cnd_t *cond) {
+ kcond_signal(cond);
+ return thrd_success;
+}
+/** @brief Broadcast to a condition variable */
+inline int cnd_broadcast(cnd_t *cond) {
+ kcond_broadcast(cond);
+ return thrd_success;
+}
+/** @brief Wait on a condition variable */
+inline int cnd_wait(cnd_t *cond, mtx_t *mutex) {
+ kcond_wait(cond, mutex);
+ return thrd_success;
+}
+
+/** @brief Wait on a condition variable with timeout */
+int cnd_timedwait(cnd_t *cond, mtx_t *mutex, const struct timespec *time_point);
+
+/** @brief Destroy a condition variable */
+inline void cnd_destroy(cnd_t *cond) {
+ kcond_destroy(cond);
+}
+
+#endif
diff --git a/include/pifile.h b/include/pifile.h
new file mode 100644
index 000000000..5022dcc07
--- /dev/null
+++ b/include/pifile.h
@@ -0,0 +1,68 @@
+/**
+ * @file pifile.h
+ * @author Giovanni Bajo
+ * @brief Load a PI-mapped file
+ * @ingroup asset
+ *
+ * This module exports a virtual filesystem called "pi:/" that allows to
+ * access a portion of the bus as if it was a file (PI-mapped files).
+ *
+ * This is sometimes needed for specific use cases, for instance:
+ *
+ * * Flashcarts might allow to load data from the SD card directly into
+ * the internal SDRAM, which is mapped to the PI bus. If you want then
+ * to access the file, you can use the "pi:/" filesystem.
+ * * Some applications might embed data directly into the ROM without using
+ * the DragonFS, and they might want to access it directly from the PI bus
+ * as if the data was a file.
+ *
+ * Normally, you should use the DragonFS filesystem to embed assets in ROM,
+ * which is more flexible, but this could be useful for specific cases.
+ *
+ * To open a file from the PI bus, use the standard fopen function with
+ * a filename starting with "pi:/". For instance, to open a file at address
+ * 0x10400000 with length 0xF000, use "pi:/10400000:F000". Alternatively,
+ * you can use the #pifile_open function to open the file.
+ *
+ * Notice that the specified address must be a PI bus address (not a CPU
+ * virtual address). All accesses will be performed using DMA so the
+ * full 32-bit PI address space is accessible.
+ *
+ */
+
+#ifndef LIBDRAGON_PIFILE_H
+#define LIBDRAGON_PIFILE_H
+
+#include "preview.h"
+#include "n64types.h"
+#include
+
+/**
+ * @brief Initialize the PI file system
+ * @preview
+ *
+ * This function initializes the PI file system. After calling this function,
+ * the "pi:/" filesystem will be available to open files via fopen().
+ */
+LIBDRAGON_PREVIEW_API
+void pifile_init(void);
+
+/**
+ * @brief Open a file from the PI bus given a base address and size
+ * @preview
+ *
+ * This function opens a "file" on the PI bus. Since it does not rely on
+ * a filesystem, the "file" is just described as a base address and size.
+ *
+ * The file is opened in read-only mode. Read-write mode is currently not supported.
+ *
+ * @param[in] base Base address of the file
+ * @param[in] size Size of the file
+ * @param[in] flags Flags to open the file with, for future use. Use 0.
+ *
+ * @return FILE pointer to use with standard C functions (fread, fclose)
+ */
+LIBDRAGON_PREVIEW_API
+FILE *pifile_open(pi_addr_t base, size_t size, uint32_t flags);
+
+#endif
diff --git a/include/pputils.h b/include/pputils.h
index 5b136754e..c3543c121 100644
--- a/include/pputils.h
+++ b/include/pputils.h
@@ -1,7 +1,7 @@
/**
* @file pputils.h
- * @author Dennis Heinze
* @author Giovanni Bajo
+ * @author Dennis Heinze
*/
/*
* Preprocessor metaprogramming utils
diff --git a/include/preview.h b/include/preview.h
new file mode 100644
index 000000000..5e5aa92bc
--- /dev/null
+++ b/include/preview.h
@@ -0,0 +1,158 @@
+/**
+ * @file preview.h
+ * @brief Preview APIs
+ * @ingroup preview
+ */
+
+/**
+ * @defgroup preview Preview APIs
+ * @ingroup libdragon
+ * @brief Preview APIs may change without notice
+ *
+ * libdragon's APIs are either stable or preview.
+ * Stable APIs are guaranteed to remain working without breakage.
+ * Preview APIs may change without notice, potentially causing project breakage.
+ *
+ * Preview APIs are locked by default: using one is a compile time error, unless
+ * the project opts in by setting `LIBDRAGON_PREVIEW` in its Makefile, before
+ * including n64.mk:
+ *
+ * * `0` — using a preview API is a hard error (default)
+ * * `1` — preview APIs are usable but each use produces a warning
+ * * `2` — preview APIs are fully unlocked (no diagnostics)
+ *
+ * There are three ways of marking an API as preview:
+ *
+ * * #LIBDRAGON_PREVIEW_HEADER locks a whole header. This is only meant for the
+ * few headers that the user is expected to include explicitly (eg: GL/gl.h).
+ * * #LIBDRAGON_PREVIEW_API locks a function.
+ * * #LIBDRAGON_PREVIEW_SYM locks non-function
+ * declarations. Most headers are pulled in by libdragon.h, so they must stay
+ * includable even in stable projects.
+ *
+ * @note libdragon itself is always built with preview enabled, so a single
+ * libdragon.a serves both stable and preview projects. This means that a
+ * preview API must never change the layout of a type, the body of an
+ * inline function or the value of a macro that stable code can also see,
+ * otherwise the library and the project would silently disagree on the
+ * ABI. Stable and preview builds must only differ in *which* declarations
+ * are usable, never in *how* they are defined. For the same reason, a
+ * preview type must never appear in a stable structure nor in the
+ * signature of a stable function, as that would make the stable API
+ * unstable as well. New fields on a stable struct must stay in the
+ * layout for everyone and be locked with #LIBDRAGON_PREVIEW_SYM: hiding
+ * them behind "#if" would change @c sizeof between the library and a
+ * stable project.
+ */
+
+#ifndef __LIBDRAGON_PREVIEW_H
+#define __LIBDRAGON_PREVIEW_H
+
+/**
+ * @def LIBDRAGON_HAVE_PREVIEW
+ * @brief 0 or 1 depending on whether preview APIs are enabled
+ *
+ * True for both `LIBDRAGON_PREVIEW=1` (warning mode) and
+ * `LIBDRAGON_PREVIEW=2` (fully unlocked).
+ */
+
+/**
+ * @def LIBDRAGON_PREVIEW_HEADER
+ * @brief Gate a whole header when preview APIs are disabled
+ *
+ * Put this at the top of a header to make its inclusion an error (or a warning
+ * in warning mode). Only use it for headers that are not reachable from
+ * libdragon.h, otherwise every stable project would fail to build. Mark single
+ * entry points with #LIBDRAGON_PREVIEW_API or #LIBDRAGON_PREVIEW_SYM instead.
+ */
+
+/**
+ * @def LIBDRAGON_PREVIEW_API
+ * @brief Mark a function as preview
+ *
+ * Calling a function with this attribute is an error if preview APIs are
+ * locked, a warning in warning mode, and a no-op when preview is fully
+ * unlocked. The @c deprecated attribute is there as a backstop: the @c error
+ * attribute is only reported for calls that survive optimization, so on its own
+ * it would miss eg. taking the address of the function, or calling it from dead
+ * code.
+ */
+
+/**
+ * @def LIBDRAGON_PREVIEW_SYM
+ * @brief Mark a field, enumerator or global as preview
+ *
+ * Use this like a GCC attribute on the declaration. The declaration stays in
+ * the header (so struct layouts do not change between stable and preview), but
+ * using it is an error when preview APIs are locked, or a warning in warning
+ * mode.
+ *
+ * @code{.c}
+ * struct bar {
+ * int a;
+ * LIBDRAGON_PREVIEW_SYM int b; // always present in the layout
+ * };
+ * enum { OLD = 0, NEW LIBDRAGON_PREVIEW_SYM = 1 };
+ * @endcode
+ *
+ * Do **not** put this on a typedef that is still mentioned later in the same
+ * header (eg: in function signatures): @c unavailable would then make the
+ * header itself fail to compile under stable.
+ *
+ * @note On GCC, designated initializers (@c .field = ...) of an unavailable
+ * field are currently not diagnosed (Clang does diagnose them).
+ * @note In warning mode this expands to @c deprecated rather than
+ * @c unavailable, because the latter is always a hard error.
+ */
+
+/// @cond
+#define __LIBDRAGON_PREVIEW_PRAGMA_RAW(x) _Pragma(#x)
+// Extra indirection level, so that macros in the argument are expanded before
+// being stringified into the pragma.
+#define __LIBDRAGON_PREVIEW_PRAGMA(x) __LIBDRAGON_PREVIEW_PRAGMA_RAW(x)
+
+// Diagnostic message shown when a preview API is used by mistake
+#define __LIBDRAGON_PREVIEW_MSG "libdragon preview API: set LIBDRAGON_PREVIEW=1 (or 2) in your Makefile (before including n64.mk) to use it"
+#define __LIBDRAGON_PREVIEW_MSG_WARN "libdragon preview API: set LIBDRAGON_PREVIEW=2 in your Makefile (before including n64.mk) to silence this"
+/// @endcond
+
+// __LIBDRAGON_INTERNAL_BUILD is only defined while building libdragon itself,
+// it allows use of preview APIs so that libdragon can be built at all.
+#if defined(__LIBDRAGON_INTERNAL_BUILD)
+#define LIBDRAGON_HAVE_PREVIEW 1
+#define LIBDRAGON_PREVIEW_HEADER
+#define LIBDRAGON_PREVIEW_API
+#define LIBDRAGON_PREVIEW_SYM
+#elif defined(LIBDRAGON_PREVIEW) && (LIBDRAGON_PREVIEW) == 1
+#define LIBDRAGON_HAVE_PREVIEW 1
+// Whole-header gate: declare a deprecated unused object so including the
+// header warns, without tripping -Werror (n64.mk exempts deprecated-declarations).
+// Installed headers live on the compiler's system include path, where GCC
+// normally suppresses diagnostics — temporarily enable -Wsystem-headers so the
+// warning still fires. __LINE__ keeps the names unique across gated headers.
+#define __LIBDRAGON_PREVIEW_HEADER_PASTE2(a, b) a##b
+#define __LIBDRAGON_PREVIEW_HEADER_PASTE(a, b) __LIBDRAGON_PREVIEW_HEADER_PASTE2(a, b)
+#define __LIBDRAGON_PREVIEW_HEADER_IMPL(uniq) \
+ _Pragma("GCC diagnostic push") \
+ _Pragma("GCC diagnostic warning \"-Wsystem-headers\"") \
+ typedef int __attribute__((deprecated(__LIBDRAGON_PREVIEW_MSG_WARN))) \
+ __LIBDRAGON_PREVIEW_HEADER_PASTE(__libdragon_ph_t, uniq); \
+ static __LIBDRAGON_PREVIEW_HEADER_PASTE(__libdragon_ph_t, uniq) \
+ __LIBDRAGON_PREVIEW_HEADER_PASTE(__libdragon_ph_v, uniq) __attribute__((unused)); \
+ _Pragma("GCC diagnostic pop")
+#define LIBDRAGON_PREVIEW_HEADER __LIBDRAGON_PREVIEW_HEADER_IMPL(__LINE__)
+#define LIBDRAGON_PREVIEW_API __attribute__((deprecated(__LIBDRAGON_PREVIEW_MSG_WARN)))
+#define LIBDRAGON_PREVIEW_SYM __attribute__((deprecated(__LIBDRAGON_PREVIEW_MSG_WARN)))
+#elif defined(LIBDRAGON_PREVIEW) && (LIBDRAGON_PREVIEW) != 0
+#define LIBDRAGON_HAVE_PREVIEW 1
+#define LIBDRAGON_PREVIEW_HEADER
+#define LIBDRAGON_PREVIEW_API
+#define LIBDRAGON_PREVIEW_SYM
+#else
+#define LIBDRAGON_HAVE_PREVIEW 0
+#define LIBDRAGON_PREVIEW_HEADER __LIBDRAGON_PREVIEW_PRAGMA(GCC error __LIBDRAGON_PREVIEW_MSG)
+#define LIBDRAGON_PREVIEW_API __attribute__((error(__LIBDRAGON_PREVIEW_MSG), deprecated(__LIBDRAGON_PREVIEW_MSG)))
+#define LIBDRAGON_PREVIEW_SYM __attribute__((unavailable(__LIBDRAGON_PREVIEW_MSG)))
+#endif
+
+#endif
diff --git a/include/profile.h b/include/profile.h
new file mode 100644
index 000000000..d3bae7932
--- /dev/null
+++ b/include/profile.h
@@ -0,0 +1,300 @@
+/**
+ * @file profile.h
+ * @author Giovanni Bajo
+ * @brief CPU profiler API.
+ */
+#ifndef PROFILE_H
+#define PROFILE_H
+
+#include "preview.h"
+#include "n64sys.h"
+#include
+
+/**
+ * @brief Enable/disable of the profiler.
+ *
+ * You can define this to 0 at compile-time if you want to keep PROFILE_*
+ * calls in your code but otherwise ignore them.
+ */
+#ifndef LIBDRAGON_PROFILE
+#define LIBDRAGON_PROFILE 1
+#endif
+
+// On PC, always disable the profiler.
+#ifndef N64
+#undef LIBDRAGON_PROFILE
+#define LIBDRAGON_PROFILE 0
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** @brief Parameters for the profiler */
+typedef struct {
+ /**
+ * @brief Number of profiling slots (default: 128)
+ *
+ * This is the number of different categories that can be
+ * used to profile code sections.
+ */
+ int num_slots;
+
+ /**
+ * @brief Interval in seconds to dump profiling info to stderr (default: 5.0s)
+ *
+ * Use a negative number to disable periodic dumps. Notice that each time
+ * am automatic dump is performed at this interval, the profiler is also reset.
+ */
+ float dump_stderr_interval;
+} profile_parms_t;
+
+/**
+ * @brief Initialize the profiler.
+ * @preview
+ *
+ * Initialize the profiler. This must be called before using any of the
+ * PROFILE_* macros.
+ *
+ * @param parms Parameters for the profiler (NULL to use defaults)
+ *
+ * @see #profile_parms_t
+ */
+LIBDRAGON_PREVIEW_API
+void profile_init(profile_parms_t *parms);
+
+/**
+ * @brief Close the profiler.
+ * @preview
+ */
+LIBDRAGON_PREVIEW_API
+void profile_close(void);
+
+/**
+ * @brief Reset the profiler.
+ * @preview
+ *
+ * Reset the profiler. This will clear all accumulated profile data and reset
+ * the counters to 0.
+ *
+ * By default, this is called automatically at the interval specified by
+ * profile_parms_t::dump_stderr_interval.
+ *
+ * @see #profile_parms_t
+ * @see #profile_init
+ */
+LIBDRAGON_PREVIEW_API
+void profile_reset(void);
+
+/**
+ * @brief First profile slot reserved to each subsystem.
+ *
+ * All subsystems share a single global array of profile slots, indexed by the
+ * slot number passed to the PROFILE_* macros. Several of them can be active at
+ * the same time (eg: video playback drives both a video decoder and the mixer),
+ * so each one reserves a distinct range: overlapping slot numbers would
+ * silently sum unrelated measurements into the same counter.
+ *
+ * Applications defining their own slots must start at #PROFILE_SLOT_USER.
+ */
+#define PROFILE_SLOT_AUDIO 0 ///< Mixer, xm64, mid64, VADPCM (32 slots)
+#define PROFILE_SLOT_MPEG1 32 ///< MPEG-1 video decoder (16 slots)
+#define PROFILE_SLOT_H264 48 ///< H.264 video decoder (16 slots)
+#define PROFILE_SLOT_USER 64 ///< First slot available to applications
+
+/**
+ * @brief Register a new profile slot
+ * @preview
+ *
+ * This function register a slot to be used with the PROFILE_* macros. It allows
+ * to specify the name with which the slot will be identified in the dumps.
+ *
+ * @param slot Slot number to register
+ * @param name Name of the slot
+ * @param nest_level Number of nesting levels for this slot
+ *
+ * @see #profile_parms_t
+ * @see #profile_init
+ */
+LIBDRAGON_PREVIEW_API
+void profile_register(int slot, const char *name, int nest_level);
+
+/**
+ * @brief Communicate to the profiler the target frame rate
+ * @preview
+ *
+ * This is an optional function that can be called to communicate to the
+ * profiler the target frame rate. When the profiler is aware of the target
+ * frame rate, it will be used to show stats related to the expected deadline for
+ * each frame (eg: how much CPU percentage is used on average in each frame).
+ +
+ * @param fps Target frame rate in frames per second
+ */
+LIBDRAGON_PREVIEW_API
+void profile_set_target_fps(float fps);
+
+/**
+ * @brief Mark the start of the next frame
+ * @preview
+ *
+ * This function must be called at the end of each frame. It will accumulate the
+ * profile data for the current frame and prepare the profiler for the next frame.
+ *
+ * This function will also trigger a dump and reset of the profiler data if the
+ * dump interval has been reached (see profile_parms_t::dump_stderr_interval).
+ *
+ * It is not mandatory to call this function at all; if you want to profile
+ * individual code sections not related to the specific concept of a frame loop,
+ * you can simply use the PROFILE_* macros as you please and then call
+ * profile_dump() to obtain the results.
+ *
+ * @see #profile_parms_t
+ * @see #profile_dump
+ */
+LIBDRAGON_PREVIEW_API
+void profile_next_frame(void);
+
+/**
+ * @brief Dump the profiler data to the console
+ * @preview
+ *
+ * This function will dump the current profiler data to the debugging channel.
+ *
+ * Notice that profile data is not reset after dumping, so it is possible to call
+ * this function multiple times and see data being accumulated over time. If you
+ * want to reset the profiler data after dumping, you can call profile_reset()
+ * after calling profile_dump().
+ *
+ * @see #profile_reset
+ */
+LIBDRAGON_PREVIEW_API
+void profile_dump(void);
+
+///@cond
+// This function is *NOT* part of the public API
+inline void __profile_record(int slot, int32_t len) {
+ extern uint64_t *__profile_counters;
+ if (__profile_counters) __profile_counters[slot] += ((int64_t)len << 32) + 1;
+}
+///@endcond
+
+#if LIBDRAGON_PROFILE
+ #include "emux.h"
+ #include "pputils.h"
+
+ // Internal helpers to make the ordinal parameter optional in PROFILE_START/STOP.
+ // If omitted, it defaults to 0 (so PROFILE_START(SLOT) pairs with PROFILE_STOP(SLOT)).
+ #define __PROFILE_START_0(slot) \
+ int64_t __prof_start_##slot##_0 = TICKS_READ() - get_system_ticks(); \
+ emux_prof_start(slot); \
+ MEMORY_BARRIER();
+ #define __PROFILE_START_1(slot, n) \
+ int64_t __prof_start_##slot##_##n = TICKS_READ() - get_system_ticks(); \
+ emux_prof_start(slot); \
+ MEMORY_BARRIER();
+ #define __PROFILE_STOP_0(slot) \
+ MEMORY_BARRIER(); \
+ emux_prof_stop(slot); \
+ __profile_record(slot, TICKS_READ() - get_system_ticks() - __prof_start_##slot##_0);
+ #define __PROFILE_STOP_1(slot, n) \
+ MEMORY_BARRIER(); \
+ emux_prof_stop(slot); \
+ __profile_record(slot, TICKS_READ() - get_system_ticks() - __prof_start_##slot##_##n);
+ /**
+ * @brief Start profiling a code section
+ *
+ * This macro is used to start profiling a code section, selecting the slot
+ * in which to record the profile data:
+ *
+ * \begin{code}
+ * PROFILE_START(PHYSICS)
+ * phys_update(delta_time);
+ * PROFILE_STOP(PHYSICS)
+ * \end{code}
+ *
+ * A second optional parameter must be used with an ordinal number to allow
+ * multiple code sections with the same slot to be profiled within the same
+ * lexical scope.
+ *
+ * \begin{code}
+ * PROFILE_START(PHYSICS, 1)
+ * phys_update_first_pass(delta_time);
+ * PROFILE_STOP(PHYSICS, 1)
+ *
+ * update_something_else();
+ *
+ * PROFILE_START(PHYSICS, 2)
+ * phys_update_second_pass(delta_time);
+ * PROFILE_STOP(PHYSICS, 2)
+ * \end{code}
+ *
+ * @param slot Slot to profile
+ * @param n Optional ordinal number for multiple profile sections
+ * within the same lexical scope
+ *
+ * @see PROFILE_STOP
+ * @see PROFILE_SCOPE
+ */
+ #define PROFILE_START(slot, ...) \
+ __PPCAT(__PROFILE_START_, __HAS_VARARGS(__VA_ARGS__))(slot, ##__VA_ARGS__)
+
+ /**
+ * @brief Stop profiling a code section
+ *
+ * This macro is used to stop profiling a code section. It must match
+ * the PROFILE_START macro call for the same slot and same optional
+ * ordinal number.
+ *
+ * @param slot Slot to profile
+ * @param n Optional ordinal number for multiple profile sections
+ * within the same lexical scope
+ *
+ * @see PROFILE_START
+ * @see PROFILE_SCOPE
+ */
+ #define PROFILE_STOP(slot, ...) \
+ __PPCAT(__PROFILE_STOP_, __HAS_VARARGS(__VA_ARGS__))(slot, ##__VA_ARGS__)
+
+ /**
+ * @brief Start a scoped profiling section
+ *
+ * This macro is used to start a scoped profiling section. It will start
+ * a profiling section for the given slot and will stop it when the
+ * scope is exited.
+ *
+ * \begin{code}
+ * PROFILE_SCOPE(PHYSICS) {
+ * phys_update(delta_time);
+ * }
+ * \end{code}
+ *
+ * Notice that this macros never requires an ordinal number like #PROFILE_START,
+ * because it is not possible to have multiple code sections with the same slot
+ * within the same lexical scope.
+ *
+ * @param slot Slot to profile
+ *
+ * @see PROFILE_START
+ * @see PROFILE_STOP
+ */
+ #define PROFILE_SCOPE(slot) \
+ for (int64_t __prof_start_##slot = TICKS_READ() - get_system_ticks(), \
+ __prof_once_##slot = ({ emux_prof_start(slot); MEMORY_BARRIER(); (int64_t)1; }); \
+ __prof_once_##slot; \
+ __prof_once_##slot = 0, \
+ ({ MEMORY_BARRIER(); emux_prof_stop(slot); \
+ __profile_record(slot, TICKS_READ() - get_system_ticks() - __prof_start_##slot); }))
+#else
+ ///@cond
+ #define PROFILE_START(slot, ...) ((void)0)
+ #define PROFILE_STOP(slot, ...) ((void)0)
+ #define PROFILE_SCOPE(slot) for (bool __prof_once_##slot = true; __prof_once_##slot; __prof_once_##slot = false)
+ ///@endcond
+
+#endif /* LIBDRAGON_PROFILE */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PROFILE_H */
diff --git a/include/rdp.h b/include/rdp.h
index 24e0a3eb6..46d1e580d 100644
--- a/include/rdp.h
+++ b/include/rdp.h
@@ -2,6 +2,7 @@
* @file rdp.h
* @author Jennifer Taylor
* @author Dennis Heinze
+ * @author Giovanni Bajo
* @brief RDP: Hardware Display Interface
* @ingroup rdp
*/
diff --git a/include/rdpq.h b/include/rdpq.h
index a5e77e05f..1a3f20f4f 100644
--- a/include/rdpq.h
+++ b/include/rdpq.h
@@ -1,6 +1,7 @@
/**
* @file rdpq.h
* @author Dennis Heinze